diff --git a/docs/reference/pages/model_reference.rst b/docs/reference/pages/model_reference.rst index c1891c28f1..48e3f92eee 100644 --- a/docs/reference/pages/model_reference.rst +++ b/docs/reference/pages/model_reference.rst @@ -100,6 +100,28 @@ Database fields To set the global default for all pages, set ``Page.show_in_menus_default = True`` once where you first import the ``Page`` model. + .. attribute:: locked + + (boolean) + + When set to ``True``, the Wagtail editor will not allow any user's to edit + the content of the page. + + If ``locked_by`` is also set, only that user cat edit the page. + + .. attribute:: locked_by + + (foreign key to user model) + + The user who has currently locked the page. Only this user can edit the page. + + If this is ``None`` when ``locked`` is ``False``, nobody can edit the page. + + .. attribute:: locked_at + + (date/time) + + The date/time when the page was locked. Methods and properties ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/wagtail/admin/tests/pages/test_page_locking.py b/wagtail/admin/tests/pages/test_page_locking.py index 107c8076c3..2bac525ac6 100644 --- a/wagtail/admin/tests/pages/test_page_locking.py +++ b/wagtail/admin/tests/pages/test_page_locking.py @@ -1,6 +1,7 @@ from django.contrib.auth.models import Permission from django.test import TestCase from django.urls import reverse +from django.utils import timezone from wagtail.core.models import Page from wagtail.tests.testapp.models import SimplePage @@ -31,7 +32,10 @@ class TestLocking(TestCase, WagtailTestUtils): self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.root_page.id, ))) # Check that the page is locked - self.assertTrue(Page.objects.get(id=self.child_page.id).locked) + page = Page.objects.get(id=self.child_page.id) + self.assertTrue(page.locked) + self.assertEqual(page.locked_by, self.user) + self.assertIsNotNone(page.locked_at) def test_lock_get(self): response = self.client.get(reverse('wagtailadmin_pages:lock', args=(self.child_page.id, ))) @@ -40,11 +44,16 @@ class TestLocking(TestCase, WagtailTestUtils): self.assertEqual(response.status_code, 405) # Check that the page is still unlocked - self.assertFalse(Page.objects.get(id=self.child_page.id).locked) + page = Page.objects.get(id=self.child_page.id) + self.assertFalse(page.locked) + self.assertIsNone(page.locked_by) + self.assertIsNone(page.locked_at) def test_lock_post_already_locked(self): # Lock the page self.child_page.locked = True + self.child_page.locked_by = self.user + self.child_page.locked_at = timezone.now() self.child_page.save() response = self.client.post(reverse('wagtailadmin_pages:lock', args=(self.child_page.id, ))) @@ -53,7 +62,10 @@ class TestLocking(TestCase, WagtailTestUtils): self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.root_page.id, ))) # Check that the page is still locked - self.assertTrue(Page.objects.get(id=self.child_page.id).locked) + page = Page.objects.get(id=self.child_page.id) + self.assertTrue(page.locked) + self.assertEqual(page.locked_by, self.user) + self.assertIsNotNone(page.locked_at) def test_lock_post_with_good_redirect(self): response = self.client.post(reverse('wagtailadmin_pages:lock', args=(self.child_page.id, )), { @@ -64,7 +76,10 @@ class TestLocking(TestCase, WagtailTestUtils): self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=(self.child_page.id, ))) # Check that the page is locked - self.assertTrue(Page.objects.get(id=self.child_page.id).locked) + page = Page.objects.get(id=self.child_page.id) + self.assertTrue(page.locked) + self.assertEqual(page.locked_by, self.user) + self.assertIsNotNone(page.locked_at) def test_lock_post_with_bad_redirect(self): response = self.client.post(reverse('wagtailadmin_pages:lock', args=(self.child_page.id, )), { @@ -75,7 +90,10 @@ class TestLocking(TestCase, WagtailTestUtils): self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.root_page.id, ))) # Check that the page is locked - self.assertTrue(Page.objects.get(id=self.child_page.id).locked) + page = Page.objects.get(id=self.child_page.id) + self.assertTrue(page.locked) + self.assertEqual(page.locked_by, self.user) + self.assertIsNotNone(page.locked_at) def test_lock_post_bad_page(self): response = self.client.post(reverse('wagtailadmin_pages:lock', args=(9999, ))) @@ -84,7 +102,10 @@ class TestLocking(TestCase, WagtailTestUtils): self.assertEqual(response.status_code, 404) # Check that the page is still unlocked - self.assertFalse(Page.objects.get(id=self.child_page.id).locked) + page = Page.objects.get(id=self.child_page.id) + self.assertFalse(page.locked) + self.assertIsNone(page.locked_by) + self.assertIsNone(page.locked_at) def test_lock_post_bad_permissions(self): # Remove privileges from user @@ -100,11 +121,16 @@ class TestLocking(TestCase, WagtailTestUtils): self.assertEqual(response.status_code, 403) # Check that the page is still unlocked - self.assertFalse(Page.objects.get(id=self.child_page.id).locked) + page = Page.objects.get(id=self.child_page.id) + self.assertFalse(page.locked) + self.assertIsNone(page.locked_by) + self.assertIsNone(page.locked_at) def test_unlock_post(self): # Lock the page self.child_page.locked = True + self.child_page.locked_by = self.user + self.child_page.locked_at = timezone.now() self.child_page.save() response = self.client.post(reverse('wagtailadmin_pages:unlock', args=(self.child_page.id, ))) @@ -113,11 +139,16 @@ class TestLocking(TestCase, WagtailTestUtils): self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.root_page.id, ))) # Check that the page is unlocked - self.assertFalse(Page.objects.get(id=self.child_page.id).locked) + page = Page.objects.get(id=self.child_page.id) + self.assertFalse(page.locked) + self.assertIsNone(page.locked_by) + self.assertIsNone(page.locked_at) def test_unlock_get(self): # Lock the page self.child_page.locked = True + self.child_page.locked_by = self.user + self.child_page.locked_at = timezone.now() self.child_page.save() response = self.client.get(reverse('wagtailadmin_pages:unlock', args=(self.child_page.id, ))) @@ -126,7 +157,10 @@ class TestLocking(TestCase, WagtailTestUtils): self.assertEqual(response.status_code, 405) # Check that the page is still locked - self.assertTrue(Page.objects.get(id=self.child_page.id).locked) + page = Page.objects.get(id=self.child_page.id) + self.assertTrue(page.locked) + self.assertEqual(page.locked_by, self.user) + self.assertIsNotNone(page.locked_at) def test_unlock_post_already_unlocked(self): response = self.client.post(reverse('wagtailadmin_pages:unlock', args=(self.child_page.id, ))) @@ -135,11 +169,16 @@ class TestLocking(TestCase, WagtailTestUtils): self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.root_page.id, ))) # Check that the page is still unlocked - self.assertFalse(Page.objects.get(id=self.child_page.id).locked) + page = Page.objects.get(id=self.child_page.id) + self.assertFalse(page.locked) + self.assertIsNone(page.locked_by) + self.assertIsNone(page.locked_at) def test_unlock_post_with_good_redirect(self): # Lock the page self.child_page.locked = True + self.child_page.locked_by = self.user + self.child_page.locked_at = timezone.now() self.child_page.save() response = self.client.post(reverse('wagtailadmin_pages:unlock', args=(self.child_page.id, )), { @@ -150,11 +189,16 @@ class TestLocking(TestCase, WagtailTestUtils): self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=(self.child_page.id, ))) # Check that the page is unlocked - self.assertFalse(Page.objects.get(id=self.child_page.id).locked) + page = Page.objects.get(id=self.child_page.id) + self.assertFalse(page.locked) + self.assertIsNone(page.locked_by) + self.assertIsNone(page.locked_at) def test_unlock_post_with_bad_redirect(self): # Lock the page self.child_page.locked = True + self.child_page.locked_by = self.user + self.child_page.locked_at = timezone.now() self.child_page.save() response = self.client.post(reverse('wagtailadmin_pages:unlock', args=(self.child_page.id, )), { @@ -165,11 +209,16 @@ class TestLocking(TestCase, WagtailTestUtils): self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.root_page.id, ))) # Check that the page is unlocked - self.assertFalse(Page.objects.get(id=self.child_page.id).locked) + page = Page.objects.get(id=self.child_page.id) + self.assertFalse(page.locked) + self.assertIsNone(page.locked_by) + self.assertIsNone(page.locked_at) def test_unlock_post_bad_page(self): # Lock the page self.child_page.locked = True + self.child_page.locked_by = self.user + self.child_page.locked_at = timezone.now() self.child_page.save() response = self.client.post(reverse('wagtailadmin_pages:unlock', args=(9999, ))) @@ -178,7 +227,10 @@ class TestLocking(TestCase, WagtailTestUtils): self.assertEqual(response.status_code, 404) # Check that the page is still locked - self.assertTrue(Page.objects.get(id=self.child_page.id).locked) + page = Page.objects.get(id=self.child_page.id) + self.assertTrue(page.locked) + self.assertEqual(page.locked_by, self.user) + self.assertIsNotNone(page.locked_at) def test_unlock_post_bad_permissions(self): # Remove privileges from user @@ -190,6 +242,8 @@ class TestLocking(TestCase, WagtailTestUtils): # Lock the page self.child_page.locked = True + self.child_page.locked_by = self.user + self.child_page.locked_at = timezone.now() self.child_page.save() response = self.client.post(reverse('wagtailadmin_pages:unlock', args=(self.child_page.id, ))) @@ -198,4 +252,7 @@ class TestLocking(TestCase, WagtailTestUtils): self.assertEqual(response.status_code, 403) # Check that the page is still locked - self.assertTrue(Page.objects.get(id=self.child_page.id).locked) + page = Page.objects.get(id=self.child_page.id) + self.assertTrue(page.locked) + self.assertEqual(page.locked_by, self.user) + self.assertIsNotNone(page.locked_at) diff --git a/wagtail/admin/views/pages.py b/wagtail/admin/views/pages.py index d732feeaf7..2f84c9d662 100644 --- a/wagtail/admin/views/pages.py +++ b/wagtail/admin/views/pages.py @@ -1072,6 +1072,8 @@ def lock(request, page_id): # Lock the page if not page.locked: page.locked = True + page.locked_by = request.user + page.locked_at = timezone.now() page.save() messages.success(request, _("Page '{0}' is now locked.").format(page.get_admin_display_title())) @@ -1096,6 +1098,8 @@ def unlock(request, page_id): # Unlock the page if page.locked: page.locked = False + page.locked_by = None + page.locked_at = None page.save() messages.success(request, _("Page '{0}' is now unlocked.").format(page.get_admin_display_title())) diff --git a/wagtail/core/migrations/0042_lock_fields.py b/wagtail/core/migrations/0042_lock_fields.py new file mode 100644 index 0000000000..e33f100dde --- /dev/null +++ b/wagtail/core/migrations/0042_lock_fields.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2.6 on 2019-10-16 08:30 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('wagtailcore', '0041_group_collection_permissions_verbose_name_plural'), + ] + + operations = [ + migrations.AddField( + model_name='page', + name='locked_at', + field=models.DateTimeField(editable=False, null=True, verbose_name='locked at'), + ), + migrations.AddField( + model_name='page', + name='locked_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='locked_pages', to=settings.AUTH_USER_MODEL, verbose_name='locked by'), + ), + ] diff --git a/wagtail/core/models.py b/wagtail/core/models.py index 68f65b78cb..713ee7e4bf 100644 --- a/wagtail/core/models.py +++ b/wagtail/core/models.py @@ -296,6 +296,16 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase): expired = models.BooleanField(verbose_name=_('expired'), default=False, editable=False) 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_pages' + ) first_published_at = models.DateTimeField( verbose_name=_('first published at'), @@ -1486,6 +1496,8 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase): * ``has_unpublished_changes`` * ``owner`` * ``locked`` + * ``locked_by`` + * ``locked_at`` * ``latest_revision_created_at`` * ``first_published_at`` """