734 lines
25 KiB
Python
734 lines
25 KiB
Python
|
|
from django.contrib.admin.utils import quote
|
|||
|
|
from django.contrib.auth import get_permission_codename
|
|||
|
|
from django.contrib.auth.models import Group, Permission
|
|||
|
|
from django.test import TestCase, override_settings
|
|||
|
|
from django.urls import NoReverseMatch, reverse
|
|||
|
|
from django.utils import timezone
|
|||
|
|
|
|||
|
|
from wagtail.admin.utils import get_user_display_name
|
|||
|
|
from wagtail.locks import WorkflowLock
|
|||
|
|
from wagtail.models import GroupApprovalTask, Workflow, WorkflowTask
|
|||
|
|
from wagtail.test.testapp.models import (
|
|||
|
|
Advert,
|
|||
|
|
DraftStateModel,
|
|||
|
|
FullFeaturedSnippet,
|
|||
|
|
LockableModel,
|
|||
|
|
)
|
|||
|
|
from wagtail.test.utils import WagtailTestUtils
|
|||
|
|
|
|||
|
|
|
|||
|
|
class BaseLockingTestCase(WagtailTestUtils, TestCase):
|
|||
|
|
model = LockableModel
|
|||
|
|
|
|||
|
|
def setUp(self):
|
|||
|
|
self.user = self.login()
|
|||
|
|
self.snippet = self.model.objects.create(text="I'm a lockable snippet!")
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def model_name(self):
|
|||
|
|
return self.model._meta.verbose_name
|
|||
|
|
|
|||
|
|
def get_url(self, name, args=None):
|
|||
|
|
args = args if args is not None else [quote(self.snippet.pk)]
|
|||
|
|
return reverse(self.snippet.snippet_viewset.get_url_name(name), args=args)
|
|||
|
|
|
|||
|
|
def lock_snippet(self, user=None):
|
|||
|
|
self.snippet.locked = True
|
|||
|
|
self.snippet.locked_by = user
|
|||
|
|
self.snippet.locked_at = timezone.now()
|
|||
|
|
self.snippet.save()
|
|||
|
|
|
|||
|
|
def refresh_snippet(self):
|
|||
|
|
self.snippet.refresh_from_db()
|
|||
|
|
|
|||
|
|
def set_permissions(self, permission_names, user=None):
|
|||
|
|
if user is None:
|
|||
|
|
user = self.user
|
|||
|
|
|
|||
|
|
user.is_superuser = False
|
|||
|
|
|
|||
|
|
permissions = [
|
|||
|
|
Permission.objects.get(
|
|||
|
|
content_type__app_label="wagtailadmin",
|
|||
|
|
codename="access_admin",
|
|||
|
|
)
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
for name in permission_names:
|
|||
|
|
permissions.append(
|
|||
|
|
Permission.objects.get(
|
|||
|
|
content_type__app_label="tests",
|
|||
|
|
codename=get_permission_codename(name, self.model._meta),
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
user.user_permissions.set(permissions)
|
|||
|
|
user.save()
|
|||
|
|
|
|||
|
|
|
|||
|
|
class DraftStateModelTestCase:
|
|||
|
|
model = DraftStateModel
|
|||
|
|
|
|||
|
|
def refresh_snippet(self):
|
|||
|
|
self.snippet.refresh_from_db()
|
|||
|
|
self.snippet = self.snippet.get_latest_revision_as_object()
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestLocking(BaseLockingTestCase):
|
|||
|
|
def test_lock_post(self):
|
|||
|
|
response = self.client.post(self.get_url("lock"))
|
|||
|
|
self.refresh_snippet()
|
|||
|
|
|
|||
|
|
# Check response
|
|||
|
|
self.assertRedirects(response, self.get_url("edit"))
|
|||
|
|
|
|||
|
|
# Check that the snippet is locked
|
|||
|
|
self.assertTrue(self.snippet.locked)
|
|||
|
|
self.assertEqual(self.snippet.locked_by, self.user)
|
|||
|
|
self.assertIsNotNone(self.snippet.locked_at)
|
|||
|
|
|
|||
|
|
def test_lock_get(self):
|
|||
|
|
response = self.client.get(self.get_url("lock"))
|
|||
|
|
self.refresh_snippet()
|
|||
|
|
|
|||
|
|
# Check response
|
|||
|
|
self.assertEqual(response.status_code, 405)
|
|||
|
|
|
|||
|
|
# Check that the snippet is still unlocked
|
|||
|
|
self.assertFalse(self.snippet.locked)
|
|||
|
|
self.assertIsNone(self.snippet.locked_by)
|
|||
|
|
self.assertIsNone(self.snippet.locked_at)
|
|||
|
|
|
|||
|
|
def test_lock_post_already_locked(self):
|
|||
|
|
# Lock the snippet
|
|||
|
|
self.lock_snippet(self.user)
|
|||
|
|
|
|||
|
|
response = self.client.post(self.get_url("lock"))
|
|||
|
|
self.refresh_snippet()
|
|||
|
|
|
|||
|
|
# Check response
|
|||
|
|
self.assertRedirects(response, self.get_url("edit"))
|
|||
|
|
|
|||
|
|
# Check that the snippet is still locked
|
|||
|
|
self.assertTrue(self.snippet.locked)
|
|||
|
|
self.assertEqual(self.snippet.locked_by, self.user)
|
|||
|
|
self.assertIsNotNone(self.snippet.locked_at)
|
|||
|
|
|
|||
|
|
def test_lock_post_with_good_redirect(self):
|
|||
|
|
next_url = self.get_url("list", args=[])
|
|||
|
|
response = self.client.post(self.get_url("lock"), {"next": next_url})
|
|||
|
|
self.refresh_snippet()
|
|||
|
|
|
|||
|
|
# Check response
|
|||
|
|
self.assertRedirects(response, next_url)
|
|||
|
|
|
|||
|
|
# Check that the snippet is locked
|
|||
|
|
self.assertTrue(self.snippet.locked)
|
|||
|
|
self.assertEqual(self.snippet.locked_by, self.user)
|
|||
|
|
self.assertIsNotNone(self.snippet.locked_at)
|
|||
|
|
|
|||
|
|
def test_lock_post_with_bad_redirect(self):
|
|||
|
|
response = self.client.post(
|
|||
|
|
self.get_url("lock"),
|
|||
|
|
{"next": "http://www.google.co.uk"},
|
|||
|
|
)
|
|||
|
|
self.refresh_snippet()
|
|||
|
|
|
|||
|
|
# Check response
|
|||
|
|
self.assertRedirects(response, self.get_url("edit"))
|
|||
|
|
|
|||
|
|
# Check that the snippet is locked
|
|||
|
|
self.assertTrue(self.snippet.locked)
|
|||
|
|
self.assertEqual(self.snippet.locked_by, self.user)
|
|||
|
|
self.assertIsNotNone(self.snippet.locked_at)
|
|||
|
|
|
|||
|
|
def test_lock_post_bad_snippet(self):
|
|||
|
|
response = self.client.post(self.get_url("edit", args=[quote(9999999)]))
|
|||
|
|
self.refresh_snippet()
|
|||
|
|
|
|||
|
|
# Check response
|
|||
|
|
self.assertEqual(response.status_code, 404)
|
|||
|
|
|
|||
|
|
# Check that the snippet is still unlocked
|
|||
|
|
self.assertFalse(self.snippet.locked)
|
|||
|
|
self.assertIsNone(self.snippet.locked_by)
|
|||
|
|
self.assertIsNone(self.snippet.locked_at)
|
|||
|
|
|
|||
|
|
def test_lock_post_not_enabled_snippet(self):
|
|||
|
|
self.snippet = Advert.objects.create(text="I'm a non-lockable snippet!")
|
|||
|
|
|
|||
|
|
with self.assertRaises(NoReverseMatch):
|
|||
|
|
self.client.post(self.get_url("lock"))
|
|||
|
|
|
|||
|
|
def test_lock_post_bad_permissions(self):
|
|||
|
|
# Remove privileges from user
|
|||
|
|
self.set_permissions([])
|
|||
|
|
|
|||
|
|
response = self.client.post(self.get_url("lock"))
|
|||
|
|
self.refresh_snippet()
|
|||
|
|
|
|||
|
|
# Check response
|
|||
|
|
self.assertRedirects(response, reverse("wagtailadmin_home"))
|
|||
|
|
|
|||
|
|
# Check that the snippet is still unlocked
|
|||
|
|
self.assertFalse(self.snippet.locked)
|
|||
|
|
self.assertIsNone(self.snippet.locked_by)
|
|||
|
|
self.assertIsNone(self.snippet.locked_at)
|
|||
|
|
|
|||
|
|
def test_unlock_post(self):
|
|||
|
|
# Lock the snippet
|
|||
|
|
self.lock_snippet(self.user)
|
|||
|
|
|
|||
|
|
response = self.client.post(self.get_url("unlock"))
|
|||
|
|
self.refresh_snippet()
|
|||
|
|
|
|||
|
|
# Check response
|
|||
|
|
self.assertRedirects(response, self.get_url("edit"))
|
|||
|
|
|
|||
|
|
# Check that the snippet is unlocked
|
|||
|
|
self.assertFalse(self.snippet.locked)
|
|||
|
|
self.assertIsNone(self.snippet.locked_by)
|
|||
|
|
self.assertIsNone(self.snippet.locked_at)
|
|||
|
|
|
|||
|
|
def test_unlock_get(self):
|
|||
|
|
# Lock the snippet
|
|||
|
|
self.lock_snippet(self.user)
|
|||
|
|
|
|||
|
|
response = self.client.get(self.get_url("unlock"))
|
|||
|
|
self.refresh_snippet()
|
|||
|
|
|
|||
|
|
# Check response
|
|||
|
|
self.assertEqual(response.status_code, 405)
|
|||
|
|
|
|||
|
|
# Check that the snippet is still locked
|
|||
|
|
self.assertTrue(self.snippet.locked)
|
|||
|
|
self.assertEqual(self.snippet.locked_by, self.user)
|
|||
|
|
self.assertIsNotNone(self.snippet.locked_at)
|
|||
|
|
|
|||
|
|
def test_unlock_post_already_unlocked(self):
|
|||
|
|
response = self.client.post(self.get_url("unlock"))
|
|||
|
|
self.refresh_snippet()
|
|||
|
|
|
|||
|
|
# Check response
|
|||
|
|
self.assertRedirects(response, self.get_url("edit"))
|
|||
|
|
|
|||
|
|
# Check that the snippet is still unlocked
|
|||
|
|
self.assertFalse(self.snippet.locked)
|
|||
|
|
self.assertIsNone(self.snippet.locked_by)
|
|||
|
|
self.assertIsNone(self.snippet.locked_at)
|
|||
|
|
|
|||
|
|
def test_unlock_post_with_good_redirect(self):
|
|||
|
|
# Lock the snippet
|
|||
|
|
self.lock_snippet(self.user)
|
|||
|
|
|
|||
|
|
next_url = self.get_url("list", args=[])
|
|||
|
|
response = self.client.post(self.get_url("unlock"), {"next": next_url})
|
|||
|
|
self.refresh_snippet()
|
|||
|
|
|
|||
|
|
# Check response
|
|||
|
|
self.assertRedirects(response, next_url)
|
|||
|
|
|
|||
|
|
# Check that the snippet is unlocked
|
|||
|
|
self.assertFalse(self.snippet.locked)
|
|||
|
|
self.assertIsNone(self.snippet.locked_by)
|
|||
|
|
self.assertIsNone(self.snippet.locked_at)
|
|||
|
|
|
|||
|
|
def test_unlock_post_with_bad_redirect(self):
|
|||
|
|
# Lock the snippet
|
|||
|
|
self.lock_snippet(self.user)
|
|||
|
|
|
|||
|
|
response = self.client.post(
|
|||
|
|
self.get_url("unlock"),
|
|||
|
|
{"next": "http://www.google.co.uk"},
|
|||
|
|
)
|
|||
|
|
self.refresh_snippet()
|
|||
|
|
|
|||
|
|
# Check response
|
|||
|
|
self.assertRedirects(response, self.get_url("edit"))
|
|||
|
|
|
|||
|
|
# Check that the snippet is unlocked
|
|||
|
|
self.assertFalse(self.snippet.locked)
|
|||
|
|
self.assertIsNone(self.snippet.locked_by)
|
|||
|
|
self.assertIsNone(self.snippet.locked_at)
|
|||
|
|
|
|||
|
|
def test_unlock_post_bad_snippet(self):
|
|||
|
|
# Lock the snippet
|
|||
|
|
self.lock_snippet(self.user)
|
|||
|
|
|
|||
|
|
response = self.client.post(self.get_url("unlock", args=[quote(9999999)]))
|
|||
|
|
self.refresh_snippet()
|
|||
|
|
|
|||
|
|
# Check response
|
|||
|
|
self.assertEqual(response.status_code, 404)
|
|||
|
|
|
|||
|
|
# Check that the snippet is still locked
|
|||
|
|
self.assertTrue(self.snippet.locked)
|
|||
|
|
self.assertEqual(self.snippet.locked_by, self.user)
|
|||
|
|
self.assertIsNotNone(self.snippet.locked_at)
|
|||
|
|
|
|||
|
|
def test_unlock_post_not_enabled_snippet(self):
|
|||
|
|
self.snippet = Advert.objects.create(text="I'm a non-lockable snippet!")
|
|||
|
|
with self.assertRaises(NoReverseMatch):
|
|||
|
|
self.client.post(self.get_url("unlock"))
|
|||
|
|
|
|||
|
|
def test_unlock_post_bad_permissions(self):
|
|||
|
|
# Remove privileges from user
|
|||
|
|
self.user.is_superuser = False
|
|||
|
|
self.user.groups.add(Group.objects.get(name="Editors"))
|
|||
|
|
self.user.save()
|
|||
|
|
|
|||
|
|
# Lock the snippet
|
|||
|
|
self.lock_snippet(self.create_user("user2"))
|
|||
|
|
|
|||
|
|
response = self.client.post(self.get_url("unlock"))
|
|||
|
|
self.refresh_snippet()
|
|||
|
|
|
|||
|
|
# Check response
|
|||
|
|
self.assertEqual(response.status_code, 302)
|
|||
|
|
|
|||
|
|
# Check that the snippet is still locked
|
|||
|
|
self.assertTrue(self.snippet.locked)
|
|||
|
|
self.assertIsNotNone(self.snippet.locked_at)
|
|||
|
|
|
|||
|
|
def test_unlock_post_own_snippet_with_bad_permissions(self):
|
|||
|
|
"""User can unlock a snippet they have locked without the unlock permission."""
|
|||
|
|
|
|||
|
|
# Remove privileges from user
|
|||
|
|
self.user.is_superuser = False
|
|||
|
|
self.user.groups.add(Group.objects.get(name="Editors"))
|
|||
|
|
self.user.save()
|
|||
|
|
|
|||
|
|
# Lock the snippet
|
|||
|
|
self.lock_snippet(self.user)
|
|||
|
|
|
|||
|
|
next_url = reverse("wagtailadmin_home")
|
|||
|
|
response = self.client.post(self.get_url("unlock"), {"next": next_url})
|
|||
|
|
self.refresh_snippet()
|
|||
|
|
|
|||
|
|
# Check response
|
|||
|
|
self.assertRedirects(response, next_url)
|
|||
|
|
|
|||
|
|
# Check that the snippet is not locked
|
|||
|
|
self.assertFalse(self.snippet.locked)
|
|||
|
|
self.assertIsNone(self.snippet.locked_by)
|
|||
|
|
self.assertIsNone(self.snippet.locked_at)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestLockingWithDraftState(DraftStateModelTestCase, TestLocking):
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestEditLockedSnippet(BaseLockingTestCase):
|
|||
|
|
save_button_label = "Save"
|
|||
|
|
|
|||
|
|
def test_edit_post_locked_by_another_user(self):
|
|||
|
|
"""A user cannot edit a snippet that is locked by another user."""
|
|||
|
|
# Lock the snippet
|
|||
|
|
self.lock_snippet(self.create_user("user2"))
|
|||
|
|
|
|||
|
|
# Try to edit the snippet
|
|||
|
|
response = self.client.post(
|
|||
|
|
self.get_url("edit"),
|
|||
|
|
{"text": "Edited while locked"},
|
|||
|
|
)
|
|||
|
|
self.refresh_snippet()
|
|||
|
|
|
|||
|
|
# Should show lock message
|
|||
|
|
self.assertContains(
|
|||
|
|
response,
|
|||
|
|
f"The {self.model_name} could not be saved as it is locked",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Check that the snippet is still locked
|
|||
|
|
self.assertTrue(self.snippet.locked)
|
|||
|
|
|
|||
|
|
# Check that the snippet is not edited
|
|||
|
|
self.assertEqual(self.snippet.text, "I'm a lockable snippet!")
|
|||
|
|
|
|||
|
|
def test_edit_post_locked_by_self(self):
|
|||
|
|
"""A user can edit a snippet that is locked by themselves."""
|
|||
|
|
# Lock the snippet
|
|||
|
|
self.lock_snippet(self.user)
|
|||
|
|
|
|||
|
|
# Try to edit the snippet
|
|||
|
|
response = self.client.post(
|
|||
|
|
self.get_url("edit"),
|
|||
|
|
{"text": "Edited while locked"},
|
|||
|
|
follow=True,
|
|||
|
|
)
|
|||
|
|
self.refresh_snippet()
|
|||
|
|
|
|||
|
|
# Should not show error message
|
|||
|
|
self.assertNotContains(
|
|||
|
|
response,
|
|||
|
|
f"The {self.model_name} could not be saved as it is locked",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Check that the snippet is still locked
|
|||
|
|
self.assertTrue(self.snippet.locked)
|
|||
|
|
|
|||
|
|
# Check that the snippet is edited
|
|||
|
|
self.assertEqual(self.snippet.text, "Edited while locked")
|
|||
|
|
|
|||
|
|
@override_settings(WAGTAILADMIN_GLOBAL_EDIT_LOCK=True)
|
|||
|
|
def test_edit_post_locked_by_self_with_global_lock_enabled(self):
|
|||
|
|
"""A user cannot edit a snippet that is locked by themselves if the setting is enabled."""
|
|||
|
|
# Lock the snippet
|
|||
|
|
self.lock_snippet(self.user)
|
|||
|
|
|
|||
|
|
# Try to edit the snippet
|
|||
|
|
response = self.client.post(
|
|||
|
|
self.get_url("edit"),
|
|||
|
|
{"text": "Edited while locked"},
|
|||
|
|
)
|
|||
|
|
self.refresh_snippet()
|
|||
|
|
|
|||
|
|
# Should show lock message
|
|||
|
|
self.assertContains(
|
|||
|
|
response,
|
|||
|
|
f"The {self.model_name} could not be saved as it is locked",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Check that the snippet is still locked
|
|||
|
|
self.assertTrue(self.snippet.locked)
|
|||
|
|
|
|||
|
|
# Check that the snippet is not edited
|
|||
|
|
self.assertEqual(self.snippet.text, "I'm a lockable snippet!")
|
|||
|
|
|
|||
|
|
def test_edit_get_locked_by_self(self):
|
|||
|
|
"""A user can edit and unlock a snippet that is locked by themselves."""
|
|||
|
|
cases = [
|
|||
|
|
(["change", "unlock"]),
|
|||
|
|
(["change"]), # Can unlock even without unlock permission
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
for permissions in cases:
|
|||
|
|
with self.subTest(
|
|||
|
|
"User can edit and unlock an object they have locked",
|
|||
|
|
permissions=permissions,
|
|||
|
|
):
|
|||
|
|
# Lock the snippet
|
|||
|
|
self.lock_snippet(self.user)
|
|||
|
|
|
|||
|
|
# Use the specified permissions
|
|||
|
|
self.set_permissions(permissions)
|
|||
|
|
|
|||
|
|
# Get the edit page
|
|||
|
|
response = self.client.get(self.get_url("edit"))
|
|||
|
|
html = response.content.decode()
|
|||
|
|
unlock_url = self.get_url("unlock")
|
|||
|
|
|
|||
|
|
# Should show lock message
|
|||
|
|
self.assertContains(
|
|||
|
|
response,
|
|||
|
|
"<b>'I'm a lockable snippet!' was locked</b> by <b>you</b> on",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should show Save action menu item
|
|||
|
|
self.assertContains(
|
|||
|
|
response,
|
|||
|
|
f'<em data-w-progress-target="label">{self.save_button_label}</em>',
|
|||
|
|
html=True,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should not show Locked action menu item
|
|||
|
|
self.assertTagInHTML(
|
|||
|
|
'<button type="submit" disabled>Locked</button>',
|
|||
|
|
html,
|
|||
|
|
count=0,
|
|||
|
|
allow_extra_attrs=True,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should show lock information in the side panel
|
|||
|
|
self.assertContains(
|
|||
|
|
response,
|
|||
|
|
f"Only you can make changes while the {self.model_name} is locked",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should show unlock toggle in the side panel
|
|||
|
|
self.assertTagInHTML(
|
|||
|
|
f'<input type="checkbox" checked data-action="click->w-action#post" data-controller="w-action" data-w-action-url-value="{unlock_url}">',
|
|||
|
|
html,
|
|||
|
|
count=1,
|
|||
|
|
allow_extra_attrs=True,
|
|||
|
|
)
|
|||
|
|
# Should show unlock button in the message
|
|||
|
|
self.assertTagInHTML(
|
|||
|
|
f'<button type="button" data-action="w-action#post" data-controller="w-action" data-w-action-url-value="{unlock_url}">Unlock</button>',
|
|||
|
|
html,
|
|||
|
|
count=1,
|
|||
|
|
allow_extra_attrs=True,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def test_edit_get_locked_by_another_user_has_unlock_permission(self):
|
|||
|
|
"""A user needs to unlock a snippet that's locked by another user in order to edit it."""
|
|||
|
|
user = self.create_user("user2")
|
|||
|
|
# Lock the snippet
|
|||
|
|
self.lock_snippet(user)
|
|||
|
|
|
|||
|
|
# Use edit and unlock permissions
|
|||
|
|
self.set_permissions(["change", "unlock"])
|
|||
|
|
|
|||
|
|
# Get the edit page
|
|||
|
|
response = self.client.get(self.get_url("edit"))
|
|||
|
|
html = response.content.decode()
|
|||
|
|
unlock_url = self.get_url("unlock")
|
|||
|
|
display_name = get_user_display_name(user)
|
|||
|
|
|
|||
|
|
# Should show lock message
|
|||
|
|
self.assertContains(
|
|||
|
|
response,
|
|||
|
|
f"<b>'I'm a lockable snippet!' was locked</b> by <b>{user}</b> on",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should show lock information in the side panel
|
|||
|
|
self.assertContains(
|
|||
|
|
response,
|
|||
|
|
f"Only {display_name} can make changes while the {self.model_name} is locked",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should not show Save action menu item
|
|||
|
|
self.assertNotContains(
|
|||
|
|
response,
|
|||
|
|
f'<em data-w-progress-target="label">{self.save_button_label}</em>',
|
|||
|
|
html=True,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should show Locked action menu item
|
|||
|
|
self.assertTagInHTML(
|
|||
|
|
'<button type="submit" disabled>Locked</button>',
|
|||
|
|
html,
|
|||
|
|
count=1,
|
|||
|
|
allow_extra_attrs=True,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should show unlock toggle in the side panel
|
|||
|
|
self.assertTagInHTML(
|
|||
|
|
f'<input type="checkbox" checked data-action="click->w-action#post" data-controller="w-action" data-w-action-url-value="{unlock_url}">',
|
|||
|
|
html,
|
|||
|
|
count=1,
|
|||
|
|
allow_extra_attrs=True,
|
|||
|
|
)
|
|||
|
|
# Should show unlock button in the message
|
|||
|
|
self.assertTagInHTML(
|
|||
|
|
f'<button type="button" data-action="w-action#post" data-controller="w-action" data-w-action-url-value="{unlock_url}">Unlock</button>',
|
|||
|
|
html,
|
|||
|
|
count=1,
|
|||
|
|
allow_extra_attrs=True,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def test_edit_get_locked_by_another_user_no_unlock_permission(self):
|
|||
|
|
"""
|
|||
|
|
A different user cannot unlock the object without the unlock permission.
|
|||
|
|
"""
|
|||
|
|
user = self.create_user("user2")
|
|||
|
|
# Lock the snippet
|
|||
|
|
self.lock_snippet(user)
|
|||
|
|
|
|||
|
|
# Use edit permission only
|
|||
|
|
self.set_permissions(["change"])
|
|||
|
|
|
|||
|
|
# Get the edit page
|
|||
|
|
response = self.client.get(self.get_url("edit"))
|
|||
|
|
html = response.content.decode()
|
|||
|
|
unlock_url = self.get_url("unlock")
|
|||
|
|
display_name = get_user_display_name(user)
|
|||
|
|
|
|||
|
|
# Should show lock message
|
|||
|
|
self.assertContains(
|
|||
|
|
response,
|
|||
|
|
f"<b>'I'm a lockable snippet!' was locked</b> by <b>{user}</b> on",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should show lock information in the side panel
|
|||
|
|
self.assertContains(
|
|||
|
|
response,
|
|||
|
|
f"Only {display_name} can make changes while the {self.model_name} is locked",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should not show instruction to unlock
|
|||
|
|
self.assertNotContains(response, "Unlock")
|
|||
|
|
|
|||
|
|
# Should not show Save action menu item
|
|||
|
|
self.assertNotContains(
|
|||
|
|
response,
|
|||
|
|
f'<em data-w-progress-target="label">{self.save_button_label}</em>',
|
|||
|
|
html=True,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should show Locked action menu item
|
|||
|
|
self.assertTagInHTML(
|
|||
|
|
'<button type="submit" disabled>Locked</button>',
|
|||
|
|
html,
|
|||
|
|
count=1,
|
|||
|
|
allow_extra_attrs=True,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should not show unlock toggle in the side panel
|
|||
|
|
self.assertTagInHTML(
|
|||
|
|
f'<input type="checkbox" checked data-action="click->w-action#post" data-controller="w-action" data-w-action-url-value="{unlock_url}">',
|
|||
|
|
html,
|
|||
|
|
count=0,
|
|||
|
|
allow_extra_attrs=True,
|
|||
|
|
)
|
|||
|
|
# Should not show unlock button in the message
|
|||
|
|
self.assertTagInHTML(
|
|||
|
|
f'<button type="button" data-action="w-action#post" data-controller="w-action" data-w-action-url-value="{unlock_url}">Unlock</button>',
|
|||
|
|
html,
|
|||
|
|
count=0,
|
|||
|
|
allow_extra_attrs=True,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def test_edit_get_unlocked_no_lock_permission(self):
|
|||
|
|
"""A user cannot lock an object without the lock permission."""
|
|||
|
|
# Use edit permission only
|
|||
|
|
self.set_permissions(["change"])
|
|||
|
|
|
|||
|
|
# Get the edit page
|
|||
|
|
response = self.client.get(self.get_url("edit"))
|
|||
|
|
html = response.content.decode()
|
|||
|
|
lock_url = self.get_url("lock")
|
|||
|
|
|
|||
|
|
# Should not show lock message
|
|||
|
|
self.assertNotContains(
|
|||
|
|
response,
|
|||
|
|
"<b>'I'm a lockable snippet!' was locked</b>",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should show unlocked information in the side panel
|
|||
|
|
self.assertContains(
|
|||
|
|
response,
|
|||
|
|
f"Anyone can edit this {self.model_name}",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should not show info to lock the object in the side panel
|
|||
|
|
self.assertNotContains(
|
|||
|
|
response,
|
|||
|
|
"lock it to prevent others from editing",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should show Save action menu item
|
|||
|
|
self.assertContains(
|
|||
|
|
response,
|
|||
|
|
f'<em data-w-progress-target="label">{self.save_button_label}</em>',
|
|||
|
|
html=True,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should not show Locked action menu item
|
|||
|
|
self.assertTagInHTML(
|
|||
|
|
'<button type="submit" disabled>Locked</button>',
|
|||
|
|
html,
|
|||
|
|
count=0,
|
|||
|
|
allow_extra_attrs=True,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should not show lock toggle in the side panel
|
|||
|
|
self.assertTagInHTML(
|
|||
|
|
f'<input type="checkbox" data-action="click->w-action#post" data-controller="w-action" data-w-action-url-value="{lock_url}">',
|
|||
|
|
html,
|
|||
|
|
count=0,
|
|||
|
|
allow_extra_attrs=True,
|
|||
|
|
)
|
|||
|
|
# Should not show lock button in the message
|
|||
|
|
self.assertTagInHTML(
|
|||
|
|
f'<button type="button" data-action="w-action#post" data-controller="w-action" data-w-action-url-value="{lock_url}">Lock</button>',
|
|||
|
|
html,
|
|||
|
|
count=0,
|
|||
|
|
allow_extra_attrs=True,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def test_edit_get_unlocked_has_lock_permission(self):
|
|||
|
|
"""A user can lock an object with the lock permission."""
|
|||
|
|
# Use edit and lock permissions
|
|||
|
|
self.set_permissions(["change", "lock"])
|
|||
|
|
|
|||
|
|
# Get the edit page
|
|||
|
|
response = self.client.get(self.get_url("edit"))
|
|||
|
|
html = response.content.decode()
|
|||
|
|
lock_url = self.get_url("lock")
|
|||
|
|
|
|||
|
|
# Should not show lock message
|
|||
|
|
self.assertNotContains(
|
|||
|
|
response,
|
|||
|
|
"<b>'I'm a lockable snippet!' was locked</b>",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should show unlocked information in the side panel
|
|||
|
|
self.assertContains(
|
|||
|
|
response,
|
|||
|
|
f"Anyone can edit this {self.model_name} – lock it to prevent others from editing",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should show Save action menu item
|
|||
|
|
self.assertContains(
|
|||
|
|
response,
|
|||
|
|
f'<em data-w-progress-target="label">{self.save_button_label}</em>',
|
|||
|
|
html=True,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should not show Locked action menu item
|
|||
|
|
self.assertTagInHTML(
|
|||
|
|
'<button type="submit" disabled>Locked</button>',
|
|||
|
|
html,
|
|||
|
|
count=0,
|
|||
|
|
allow_extra_attrs=True,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Should show lock toggle in the side panel
|
|||
|
|
self.assertTagInHTML(
|
|||
|
|
f'<input type="checkbox" data-action="click->w-action#post" data-controller="w-action" data-w-action-url-value="{lock_url}">',
|
|||
|
|
html,
|
|||
|
|
count=1,
|
|||
|
|
allow_extra_attrs=True,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestEditLockedDraftStateSnippet(DraftStateModelTestCase, TestEditLockedSnippet):
|
|||
|
|
save_button_label = "Save draft"
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestWorkflowLock(BaseLockingTestCase):
|
|||
|
|
model = FullFeaturedSnippet
|
|||
|
|
|
|||
|
|
def setUp(self):
|
|||
|
|
super().setUp()
|
|||
|
|
self.snippet.save_revision()
|
|||
|
|
self.moderator = self.create_user("moderator")
|
|||
|
|
self.moderators = Group.objects.get(name="Moderators")
|
|||
|
|
self.moderator.groups.add(self.moderators)
|
|||
|
|
self.set_permissions(["change"], user=self.user)
|
|||
|
|
self.set_permissions(["change", "publish"], user=self.moderator)
|
|||
|
|
|
|||
|
|
def test_when_locked_by_workflow(self):
|
|||
|
|
workflow = Workflow.objects.create(name="test_workflow")
|
|||
|
|
task = GroupApprovalTask.objects.create(name="test_task")
|
|||
|
|
task.groups.add(self.moderators)
|
|||
|
|
WorkflowTask.objects.create(workflow=workflow, task=task, sort_order=1)
|
|||
|
|
workflow.start(self.snippet, self.user)
|
|||
|
|
|
|||
|
|
lock = self.snippet.get_lock()
|
|||
|
|
self.assertIsInstance(lock, WorkflowLock)
|
|||
|
|
self.assertTrue(lock.for_user(self.user))
|
|||
|
|
self.assertFalse(lock.for_user(self.moderator))
|
|||
|
|
self.assertEqual(
|
|||
|
|
lock.get_message(self.user),
|
|||
|
|
"This full-featured snippet is currently awaiting moderation. "
|
|||
|
|
"Only reviewers for this task can edit the full-featured snippet.",
|
|||
|
|
)
|
|||
|
|
self.assertIsNone(lock.get_message(self.moderator))
|
|||
|
|
|
|||
|
|
# When visiting a snippet in a workflow with multiple tasks, the message
|
|||
|
|
# displayed to users changes to show the current task the snippet is on
|
|||
|
|
|
|||
|
|
# Add a second task to the workflow
|
|||
|
|
other_task = GroupApprovalTask.objects.create(name="another_task")
|
|||
|
|
WorkflowTask.objects.create(workflow=workflow, task=other_task, sort_order=2)
|
|||
|
|
|
|||
|
|
lock = self.snippet.get_lock()
|
|||
|
|
self.assertEqual(
|
|||
|
|
lock.get_message(self.user),
|
|||
|
|
"This full-featured snippet is awaiting <b>'test_task'</b> in the "
|
|||
|
|
"<b>'test_workflow'</b> workflow. Only reviewers for this task "
|
|||
|
|
"can edit the full-featured snippet.",
|
|||
|
|
)
|