Initial commit

This commit is contained in:
2024-08-27 20:33:44 +02:00
commit 1f1832267d
14794 changed files with 1599592 additions and 0 deletions

View File

@@ -0,0 +1,241 @@
from django.contrib.admin.utils import quote
from django.contrib.auth.models import Permission
from django.http import HttpRequest, HttpResponse
from django.test import TestCase
from django.urls import reverse
from wagtail.snippets.bulk_actions.delete import DeleteBulkAction
from wagtail.test.testapp.models import FullFeaturedSnippet
from wagtail.test.utils import WagtailTestUtils
class TestSnippetDeleteView(WagtailTestUtils, TestCase):
def setUp(self):
self.snippet_model = FullFeaturedSnippet
# create a set of test snippets
self.test_snippets = [
self.snippet_model.objects.create(
text=f"Title-{i}",
)
for i in range(1, 6)
]
self.user = self.login()
self.url = (
reverse(
"wagtail_bulk_action",
args=(
self.snippet_model._meta.app_label,
self.snippet_model._meta.model_name,
"delete",
),
)
+ "?"
)
def get_url(self, items=()):
items = items or self.test_snippets
return self.url + "&".join(f"id={item.pk}" for item in items)
def test_simple(self):
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(
response, "wagtailsnippets/bulk_actions/confirm_bulk_delete.html"
)
self.assertTemplateUsed(response, "wagtailadmin/shared/header.html")
self.assertEqual(response.context["header_icon"], "cog")
self.assertContains(response, "icon icon-cog", count=1)
def test_get_single_delete(self):
item = self.test_snippets[0]
response = self.client.get(self.get_url(items=(item,)))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(
response, "wagtailsnippets/bulk_actions/confirm_bulk_delete.html"
)
self.assertTemplateUsed(response, "wagtailadmin/shared/header.html")
self.assertEqual(response.context["header_icon"], "cog")
self.assertContains(response, "icon icon-cog", count=1)
self.assertContains(
response,
"<title>Delete full-featured snippet - Title-1 - Wagtail</title>",
html=True,
)
self.assertContains(
response,
reverse(
self.snippet_model.snippet_viewset.get_url_name("usage"),
args=(quote(item.pk),),
),
)
self.assertContains(response, "Used 0 times")
def test_bulk_delete(self):
response = self.client.post(self.get_url())
# Should redirect back to index
self.assertEqual(response.status_code, 302)
# Check that the users were deleted
for snippet in self.test_snippets:
self.assertFalse(self.snippet_model.objects.filter(pk=snippet.pk).exists())
def test_delete_with_limited_permissions(self):
self.user.is_superuser = False
self.user.user_permissions.add(
Permission.objects.get(
content_type__app_label="wagtailadmin", codename="access_admin"
)
)
self.user.save()
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
html = response.content.decode()
self.assertInHTML(
"<p>You don't have permission to delete these full-featured snippets</p>",
html,
)
for snippet in self.test_snippets:
self.assertInHTML(f"<li>{snippet.text}</li>", html)
response = self.client.post(self.get_url())
# User should be redirected back to the index
self.assertEqual(response.status_code, 302)
# Snippets should not be deleted
for snippet in self.test_snippets:
self.assertTrue(self.snippet_model.objects.filter(pk=snippet.pk).exists())
def test_before_bulk_action_hook_get(self):
with self.register_hook(
"before_bulk_action", lambda *args: HttpResponse("Overridden!")
):
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
# The hook was not called
self.assertNotEqual(response.content, b"Overridden!")
# The instances were not deleted
self.assertQuerySetEqual(
self.snippet_model.objects.filter(
pk__in=[snippet.pk for snippet in self.test_snippets]
),
self.test_snippets,
ordered=False,
)
def test_before_bulk_action_hook_post(self):
def hook_func(request, action_type, instances, action_class_instance):
self.assertIsInstance(request, HttpRequest)
self.assertEqual(action_type, "delete")
self.assertEqual(set(instances), set(self.test_snippets))
self.assertIsInstance(action_class_instance, DeleteBulkAction)
return HttpResponse("Overridden!")
with self.register_hook("before_bulk_action", hook_func):
response = self.client.post(self.get_url())
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"Overridden!")
# Request intercepted before the snippets were deleted
self.assertQuerySetEqual(
self.snippet_model.objects.filter(
pk__in=[snippet.pk for snippet in self.test_snippets]
),
self.test_snippets,
ordered=False,
)
def test_after_bulk_action_hook(self):
def hook_func(request, action_type, instances, action_class_instance):
self.assertIsInstance(request, HttpRequest)
self.assertEqual(action_type, "delete")
self.assertEqual(set(instances), set(self.test_snippets))
self.assertIsInstance(action_class_instance, DeleteBulkAction)
return HttpResponse("Overridden!")
with self.register_hook("after_bulk_action", hook_func):
response = self.client.post(self.get_url())
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"Overridden!")
# Request intercepted after the snippets were deleted
self.assertFalse(
self.snippet_model.objects.filter(
pk__in=[snippet.pk for snippet in self.test_snippets]
).exists()
)
# Also tests that the {before,after}_delete_snippet hooks are called.
# These hooks have existed since before bulk actions were introduced,
# so we need to make sure they still work.
def test_before_delete_snippet_hook_get(self):
def hook_func(request, instances):
self.assertIsInstance(request, HttpRequest)
self.assertEqual(set(instances), set(self.test_snippets))
return HttpResponse("Overridden!")
with self.register_hook("before_delete_snippet", hook_func):
response = self.client.get(self.get_url())
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"Overridden!")
# Request intercepted before the snippets were deleted
self.assertQuerySetEqual(
self.snippet_model.objects.filter(
pk__in=[snippet.pk for snippet in self.test_snippets]
),
self.test_snippets,
ordered=False,
)
def test_before_delete_snippet_hook_post(self):
def hook_func(request, instances):
self.assertIsInstance(request, HttpRequest)
self.assertEqual(set(instances), set(self.test_snippets))
return HttpResponse("Overridden!")
with self.register_hook("before_delete_snippet", hook_func):
response = self.client.post(self.get_url())
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"Overridden!")
# Request intercepted before the snippets were deleted
self.assertQuerySetEqual(
self.snippet_model.objects.filter(
pk__in=[snippet.pk for snippet in self.test_snippets]
),
self.test_snippets,
ordered=False,
)
def test_after_delete_snippet_hook(self):
def hook_func(request, instances):
self.assertIsInstance(request, HttpRequest)
self.assertEqual(set(instances), set(self.test_snippets))
return HttpResponse("Overridden!")
with self.register_hook("after_delete_snippet", hook_func):
response = self.client.post(self.get_url())
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"Overridden!")
# Request intercepted after the snippets were deleted
self.assertFalse(
self.snippet_model.objects.filter(
pk__in=[snippet.pk for snippet in self.test_snippets]
).exists()
)

View File

@@ -0,0 +1,56 @@
from django.test import TestCase
from django.urls import reverse
from wagtail.test.testapp.models import Advert, FullFeaturedSnippet
from wagtail.test.utils.wagtail_tests import WagtailTestUtils
class TestCustomModels(WagtailTestUtils, TestCase):
def setUp(self):
self.user = self.login()
def create_snippets(self, model):
return [model.objects.create(text=f"Title-{i}") for i in range(1, 6)]
def get_action_url(self, model, snippets):
return (
reverse(
"wagtail_bulk_action",
args=(
model._meta.app_label,
model._meta.model_name,
"disable",
),
)
+ "?"
+ "&".join(f"id={item.pk}" for item in snippets)
)
def get_list_url(self, model):
return reverse(model.snippet_viewset.get_url_name("list"))
def test_action_shown_for_custom_models(self):
self.create_snippets(FullFeaturedSnippet)
response = self.client.get(self.get_list_url(FullFeaturedSnippet))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Disable selected full-featured snippets")
def test_action_confirmation_accessible_for_custom_models(self):
snippets = self.create_snippets(FullFeaturedSnippet)
response = self.client.get(self.get_action_url(FullFeaturedSnippet, snippets))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(
response,
"wagtailadmin/bulk_actions/confirmation/base.html",
)
def test_action_not_shown_for_other_models(self):
self.create_snippets(Advert)
response = self.client.get(self.get_list_url(Advert))
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, "Disable selected full-featured snippets")
def test_action_confirmation_inaccessible_for_other_models(self):
snippets = self.create_snippets(Advert)
response = self.client.get(self.get_action_url(Advert, snippets))
self.assertEqual(response.status_code, 404)

View File

@@ -0,0 +1,733 @@
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&#x27;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&#x27;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&#x27;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&#x27;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&#x27;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.",
)

View File

@@ -0,0 +1,26 @@
from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.db import migrations
from django.test import TestCase
from wagtail.snippets.models import create_extra_permissions
class TestCreatePermissions(TestCase):
def setUp(self):
self.app_config = apps.get_app_config("auth")
def tearDown(self):
ContentType.objects.clear_cache()
def test_unavailable_models(self):
state = migrations.state.ProjectState()
# Unavailable contenttypes.ContentType
with self.assertNumQueries(0):
create_extra_permissions(self.app_config, verbosity=0, apps=state.apps)
# Unavailable auth.Permission
state = migrations.state.ProjectState(real_apps={"contenttypes"})
with self.assertNumQueries(0):
create_extra_permissions(self.app_config, verbosity=0, apps=state.apps)

View File

@@ -0,0 +1,509 @@
import datetime
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from freezegun import freeze_time
from wagtail.admin.views.generic.preview import PreviewOnEdit
from wagtail.test.testapp.models import (
EventCategory,
MultiPreviewModesModel,
NonPreviewableModel,
PreviewableModel,
RevisableModel,
)
from wagtail.test.utils import WagtailTestUtils
class TestPreview(WagtailTestUtils, TestCase):
def setUp(self):
self.user = self.login()
self.meetings_category = EventCategory.objects.create(name="Meetings")
self.parties_category = EventCategory.objects.create(name="Parties")
self.holidays_category = EventCategory.objects.create(name="Holidays")
self.snippet = PreviewableModel.objects.create(text="A previewable snippet")
self.preview_on_add_url = reverse(
"wagtailsnippets_tests_previewablemodel:preview_on_add"
)
self.preview_on_edit_url = reverse(
"wagtailsnippets_tests_previewablemodel:preview_on_edit",
args=(self.snippet.pk,),
)
self.session_key_prefix = "wagtail-preview-tests-previewablemodel"
self.edit_session_key = f"{self.session_key_prefix}-{self.snippet.pk}"
self.post_data = {
"text": "An edited previewable snippet",
"categories": [self.parties_category.id, self.holidays_category.id],
}
def test_preview_on_create_with_no_session_data(self):
self.assertNotIn(self.session_key_prefix, self.client.session)
response = self.client.get(self.preview_on_add_url)
# The preview should be unavailable
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/generic/preview_error.html")
self.assertContains(
response,
"<title>Preview not available - Wagtail</title>",
html=True,
)
self.assertContains(
response,
'<h1 class="preview-error__title">Preview not available</h1>',
html=True,
)
def test_preview_on_create_with_invalid_data(self):
self.assertNotIn(self.session_key_prefix, self.client.session)
response = self.client.post(self.preview_on_add_url, {"text": ""})
# Check the JSON response
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
response.content.decode(),
{"is_valid": False, "is_available": False},
)
# The invalid data should not be saved in the session
self.assertNotIn(self.session_key_prefix, self.client.session)
response = self.client.get(self.preview_on_add_url)
# The preview should still be unavailable
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/generic/preview_error.html")
self.assertContains(
response,
"<title>Preview not available - Wagtail</title>",
html=True,
)
self.assertContains(
response,
'<h1 class="preview-error__title">Preview not available</h1>',
html=True,
)
def test_preview_on_create_with_m2m_field(self):
response = self.client.post(self.preview_on_add_url, self.post_data)
# Check the JSON response
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
response.content.decode(),
{"is_valid": True, "is_available": True},
)
# Check the user can refresh the preview
self.assertIn(self.session_key_prefix, self.client.session)
response = self.client.get(self.preview_on_add_url)
# Check the HTML response
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "tests/previewable_model.html")
self.assertContains(response, "An edited previewable snippet")
self.assertContains(response, "<li>Parties</li>")
self.assertContains(response, "<li>Holidays</li>")
def test_preview_on_edit_with_m2m_field(self):
response = self.client.post(self.preview_on_edit_url, self.post_data)
# Check the JSON response
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
response.content.decode(),
{"is_valid": True, "is_available": True},
)
# Check the user can refresh the preview
self.assertIn(self.edit_session_key, self.client.session)
response = self.client.get(self.preview_on_edit_url)
# Check the HTML response
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "tests/previewable_model.html")
self.assertContains(response, "An edited previewable snippet")
self.assertContains(response, "<li>Parties</li>")
self.assertContains(response, "<li>Holidays</li>")
def test_preview_on_edit_with_valid_then_invalid_data(self):
response = self.client.post(self.preview_on_edit_url, self.post_data)
# Check the JSON response
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
response.content.decode(),
{"is_valid": True, "is_available": True},
)
# Send an invalid update request
response = self.client.post(
self.preview_on_edit_url, {**self.post_data, "text": ""}
)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
response.content.decode(),
{"is_valid": False, "is_available": True},
)
# Check the user can still see the preview with the last valid data
self.assertIn(self.edit_session_key, self.client.session)
response = self.client.get(self.preview_on_edit_url)
# Check the HTML response
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "tests/previewable_model.html")
self.assertContains(response, "An edited previewable snippet")
self.assertContains(response, "<li>Parties</li>")
self.assertContains(response, "<li>Holidays</li>")
def test_preview_on_edit_expiry(self):
initial_datetime = timezone.now()
expiry_datetime = initial_datetime + datetime.timedelta(
seconds=PreviewOnEdit.preview_expiration_timeout + 1
)
new_snippet = PreviewableModel.objects.create(text="A new previewable snippet")
with freeze_time(initial_datetime) as frozen_datetime:
response = self.client.post(self.preview_on_edit_url, self.post_data)
self.assertEqual(response.status_code, 200)
response = self.client.get(self.preview_on_edit_url)
self.assertEqual(response.status_code, 200)
frozen_datetime.move_to(expiry_datetime)
preview_url = reverse(
"wagtailsnippets_tests_previewablemodel:preview_on_edit",
args=(new_snippet.pk,),
)
response = self.client.post(preview_url, self.post_data)
self.assertEqual(response.status_code, 200)
response = self.client.get(preview_url)
self.assertEqual(response.status_code, 200)
# Stale preview data should be removed from the session
self.assertNotIn(self.edit_session_key, self.client.session)
self.assertIn(
f"{self.session_key_prefix}-{new_snippet.pk}",
self.client.session,
)
def test_preview_on_create_clear_preview_data(self):
# Set a fake preview session data for the page
self.client.session[self.session_key_prefix] = "test data"
response = self.client.delete(self.preview_on_add_url)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
response.content.decode(),
{"success": True},
)
# The data should no longer exist in the session
self.assertNotIn(self.session_key_prefix, self.client.session)
response = self.client.get(self.preview_on_add_url)
# The preview should be unavailable
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/generic/preview_error.html")
self.assertContains(
response,
"<title>Preview not available - Wagtail</title>",
html=True,
)
self.assertContains(
response,
'<h1 class="preview-error__title">Preview not available</h1>',
html=True,
)
def test_preview_on_edit_clear_preview_data(self):
# Set a fake preview session data for the page
self.client.session[self.edit_session_key] = "test data"
response = self.client.delete(self.preview_on_edit_url)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
response.content.decode(),
{"success": True},
)
# The data should no longer exist in the session
self.assertNotIn(self.edit_session_key, self.client.session)
response = self.client.get(self.preview_on_edit_url)
# The preview should be unavailable
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/generic/preview_error.html")
self.assertContains(
response,
"<title>Preview not available - Wagtail</title>",
html=True,
)
self.assertContains(
response,
'<h1 class="preview-error__title">Preview not available</h1>',
html=True,
)
def test_preview_revision(self):
snippet = MultiPreviewModesModel.objects.create(text="Multiple modes")
revision = snippet.save_revision(log_action=True)
response = self.client.get(
reverse(
"wagtailsnippets_tests_multipreviewmodesmodel:revisions_view",
args=(snippet.pk, revision.id),
)
)
self.assertEqual(response.status_code, 200)
# Should respect the default_preview_mode
self.assertTemplateUsed(response, "tests/previewable_model_alt.html")
self.assertContains(response, "Multiple modes (Alternate Preview)")
class TestEnablePreview(WagtailTestUtils, TestCase):
def setUp(self):
self.user = self.login()
self.single = PreviewableModel.objects.create(text="Single preview mode")
self.multiple = MultiPreviewModesModel.objects.create(
text="Multiple preview modes"
)
def get_url(self, snippet, name, args=None):
model_name = type(snippet)._meta.model_name
return reverse(f"wagtailsnippets_tests_{model_name}:{name}", args=args)
def test_show_preview_panel_on_create_with_single_mode(self):
create_url = self.get_url(self.single, "add")
preview_url = self.get_url(self.single, "preview_on_add")
new_tab_url = preview_url + "?mode="
response = self.client.get(create_url)
self.assertEqual(response.status_code, 200)
# Should show the preview panel
self.assertContains(response, 'data-side-panel="preview"')
self.assertContains(response, 'data-action="%s"' % preview_url)
# Should have the preview side panel toggle button
soup = self.get_soup(response.content)
toggle_button = soup.find("button", {"data-side-panel-toggle": "preview"})
self.assertIsNotNone(toggle_button)
self.assertEqual("w-tooltip w-kbd", toggle_button["data-controller"])
self.assertEqual("mod+p", toggle_button["data-w-kbd-key-value"])
# Should show the iframe
self.assertContains(
response,
'<iframe id="preview-iframe" loading="lazy" title="Preview" class="preview-panel__iframe" data-preview-iframe aria-describedby="preview-panel-error-banner">',
)
# Should show the new tab button with the default mode set
self.assertContains(response, f'href="{new_tab_url}" target="_blank"')
# Should not show the preview mode selection
self.assertNotContains(
response,
'<select id="id_preview_mode" name="preview_mode" class="preview-panel__mode-select" data-preview-mode-select>',
)
def test_show_preview_panel_on_create_with_multiple_modes(self):
create_url = self.get_url(self.multiple, "add")
preview_url = self.get_url(self.multiple, "preview_on_add")
new_tab_url = preview_url + "?mode=alt%231"
response = self.client.get(create_url)
self.assertEqual(response.status_code, 200)
# Should show the preview panel
self.assertContains(response, 'data-side-panel-toggle="preview"')
self.assertContains(response, 'data-side-panel="preview"')
self.assertContains(response, 'data-action="%s"' % preview_url)
# Should show the iframe
self.assertContains(
response,
'<iframe id="preview-iframe" loading="lazy" title="Preview" class="preview-panel__iframe" data-preview-iframe aria-describedby="preview-panel-error-banner">',
)
# Should show the new tab button with the default mode set and correctly quoted
self.assertContains(response, f'href="{new_tab_url}" target="_blank"')
# should show the preview mode selection
self.assertContains(
response,
'<select id="id_preview_mode" name="preview_mode" class="preview-panel__mode-select" data-preview-mode-select>',
)
self.assertContains(response, '<option value="">Normal</option>')
# Should respect the default_preview_mode
self.assertContains(
response, '<option value="alt#1" selected>Alternate</option>'
)
def test_show_preview_panel_on_edit_with_single_mode(self):
edit_url = self.get_url(self.single, "edit", args=(self.single.pk,))
preview_url = self.get_url(
self.single, "preview_on_edit", args=(self.multiple.pk,)
)
new_tab_url = preview_url + "?mode="
response = self.client.get(edit_url)
self.assertEqual(response.status_code, 200)
# Should show the preview panel
self.assertContains(response, 'data-side-panel-toggle="preview"')
self.assertContains(response, 'data-side-panel="preview"')
self.assertContains(response, 'data-action="%s"' % preview_url)
# Should show the iframe
self.assertContains(
response,
'<iframe id="preview-iframe" loading="lazy" title="Preview" class="preview-panel__iframe" data-preview-iframe aria-describedby="preview-panel-error-banner">',
)
# Should show the new tab button with the default mode set
self.assertContains(response, f'href="{new_tab_url}" target="_blank"')
# Should not show the preview mode selection
self.assertNotContains(
response,
'<select id="id_preview_mode" name="preview_mode" class="preview-panel__mode-select" data-preview-mode-select>',
)
def test_show_preview_panel_on_edit_with_multiple_modes(self):
edit_url = self.get_url(self.multiple, "edit", args=(self.multiple.pk,))
preview_url = self.get_url(
self.multiple, "preview_on_edit", args=(self.multiple.pk,)
)
new_tab_url = preview_url + "?mode=alt%231"
response = self.client.get(edit_url)
self.assertEqual(response.status_code, 200)
# Should show the preview panel
self.assertContains(response, 'data-side-panel-toggle="preview"')
self.assertContains(response, 'data-side-panel="preview"')
self.assertContains(response, 'data-action="%s"' % preview_url)
# Should show the iframe
self.assertContains(
response,
'<iframe id="preview-iframe" loading="lazy" title="Preview" class="preview-panel__iframe" data-preview-iframe aria-describedby="preview-panel-error-banner">',
)
# Should show the new tab button with the default mode set and correctly quoted
self.assertContains(response, f'href="{new_tab_url}" target="_blank"')
# should show the preview mode selection
self.assertContains(
response,
'<select id="id_preview_mode" name="preview_mode" class="preview-panel__mode-select" data-preview-mode-select>',
)
self.assertContains(response, '<option value="">Normal</option>')
# Should respect the default_preview_mode
self.assertContains(
response, '<option value="alt#1" selected>Alternate</option>'
)
def test_show_preview_on_revisions_list(self):
latest_revision = self.multiple.save_revision(log_action=True)
history_url = self.get_url(self.multiple, "history", args=(self.multiple.pk,))
preview_url = self.get_url(
self.multiple,
"revisions_view",
args=(self.multiple.pk, latest_revision.id),
)
response = self.client.get(history_url)
self.assertContains(response, "Preview")
self.assertContains(response, preview_url)
class TestDisablePreviewWithEmptyModes(WagtailTestUtils, TestCase):
"""
Preview can be disabled by setting preview_modes to an empty list.
"""
# NonPreviewableModel has preview_modes = []
model = NonPreviewableModel
def setUp(self):
self.user = self.login()
self.snippet = self.model.objects.create(text="A non-previewable snippet")
self.model_name = self.model._meta.model_name
def get_url(self, name, args=None):
return reverse(f"wagtailsnippets_tests_{self.model_name}:{name}", args=args)
def test_disable_preview_on_create(self):
response = self.client.get(self.get_url("add"))
self.assertEqual(response.status_code, 200)
preview_url = self.get_url("preview_on_add")
self.assertNotContains(response, 'data-side-panel-toggle="preview"')
self.assertNotContains(response, 'data-side-panel="preview"')
self.assertNotContains(response, 'data-action="%s"' % preview_url)
def test_disable_preview_on_edit(self):
response = self.client.get(self.get_url("edit", args=(self.snippet.pk,)))
self.assertEqual(response.status_code, 200)
preview_url = self.get_url("preview_on_edit", args=(self.snippet.pk,))
self.assertNotContains(response, 'data-side-panel-toggle="preview"')
self.assertNotContains(response, 'data-side-panel="preview"')
self.assertNotContains(response, 'data-action="%s"' % preview_url)
def test_disable_preview_on_revisions_list(self):
latest_revision = self.snippet.save_revision(log_action=True)
response = self.client.get(self.get_url("history", args=(self.snippet.pk,)))
preview_url = self.get_url(
"revisions_view", args=(self.snippet.pk, latest_revision.id)
)
self.assertNotContains(response, preview_url)
soup = self.get_soup(response.content)
preview_link = soup.find("a", {"href": preview_url})
self.assertIsNone(preview_link)
class TestDisablePreviewWithoutMixin(TestDisablePreviewWithEmptyModes):
"""
Preview can be disabled by not extending PreviewableMixin.
"""
# RevisableModel does not extend PreviewableMixin
model = RevisableModel
def get_url(self, name, args=None):
# Cannot use reverse() as the urls are not registered
# if the model does not extend PreviewableMixin
if name == "preview_on_add":
return f"/admin/snippets/tests/{self.model_name}/preview/"
if name == "preview_on_edit":
return f"/admin/snippets/tests/{self.model_name}/preview/{args[0]}/"
if name == "revisions_view":
return (
f"/admin/snippets/tests/{self.model_name}/history/"
f"{args[0]}/revisions/{args[1]}/view/"
)
return super().get_url(name, args)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,209 @@
from io import StringIO
from django.contrib.admin.utils import quote
from django.contrib.auth.models import Permission
from django.core import management
from django.test import TestCase
from django.urls import reverse
from wagtail.models import Page, ReferenceIndex
from wagtail.test.testapp.models import (
Advert,
DraftStateModel,
EventPage,
GenericSnippetPage,
)
from wagtail.test.utils import WagtailTestUtils
class TestUsageCount(TestCase):
fixtures = ["test.json"]
@classmethod
def setUpTestData(cls):
super().setUpTestData()
output = StringIO()
management.call_command("rebuild_references_index", stdout=output)
def test_snippet_usage_count(self):
advert = Advert.objects.get(pk=1)
self.assertEqual(ReferenceIndex.get_grouped_references_to(advert).count(), 2)
class TestUsedBy(TestCase):
fixtures = ["test.json"]
@classmethod
def setUpTestData(cls):
super().setUpTestData()
output = StringIO()
management.call_command("rebuild_references_index", stdout=output)
def test_snippet_used_by(self):
advert = Advert.objects.get(pk=1)
usage = ReferenceIndex.get_grouped_references_to(advert)
self.assertIsInstance(usage[0], tuple)
self.assertIsInstance(usage[0][0], Page)
self.assertIsInstance(usage[0][1], list)
self.assertIsInstance(usage[0][1][0], ReferenceIndex)
class TestSnippetUsageView(WagtailTestUtils, TestCase):
fixtures = ["test.json"]
def setUp(self):
self.user = self.login()
def test_use_latest_draft_as_title(self):
snippet = DraftStateModel.objects.create(text="Draft-enabled Foo, Published")
snippet.save_revision().publish()
snippet.text = "Draft-enabled Bar, In Draft"
snippet.save_revision()
response = self.client.get(
reverse(
"wagtailsnippets_tests_draftstatemodel:usage",
args=[quote(snippet.pk)],
)
)
soup = self.get_soup(response.content)
sublabel = soup.select_one(".w-breadcrumbs__sublabel")
# Should use the latest draft title in the breadcrumbs sublabel
self.assertEqual(sublabel.get_text(strip=True), "Draft-enabled Bar, In Draft")
def test_usage(self):
# resave so that usage count gets updated
page = Page.objects.get(pk=2)
page.save()
gfk_page = GenericSnippetPage(
title="Foobar Title",
snippet_content_object=Advert.objects.get(pk=1),
)
page.add_child(instance=gfk_page)
response = self.client.get(
reverse(
"wagtailsnippets_tests_advert:usage",
args=["1"],
)
)
self.assertContains(response, "Welcome to the Wagtail test site!")
self.assertContains(response, "Foobar Title")
self.assertContains(response, "<td>Generic snippet page</td>", html=True)
self.assertContains(response, "Snippet content object")
self.assertContains(response, "<th>Field</th>", html=True)
self.assertNotContains(response, "<th>If you confirm deletion</th>", html=True)
self.assertContains(response, "Snippet content object")
def test_usage_without_edit_permission_on_snippet(self):
# Create a user with basic admin backend access
user = self.create_user(
username="basicadmin", email="basicadmin@example.com", password="password"
)
admin_permission = Permission.objects.get(
content_type__app_label="wagtailadmin", codename="access_admin"
)
user.user_permissions.add(admin_permission)
self.login(username="basicadmin", password="password")
response = self.client.get(
reverse(
"wagtailsnippets_tests_advert:usage",
args=["1"],
)
)
self.assertEqual(response.status_code, 302)
self.assertRedirects(response, reverse("wagtailadmin_home"))
def test_usage_without_edit_permission_on_page(self):
# resave so that usage count gets updated
page = Page.objects.get(pk=2)
page.save()
# Create a user with edit access to snippets but not pages
user = self.create_user(
username="basicadmin", email="basicadmin@example.com", password="password"
)
admin_permission = Permission.objects.get(
content_type__app_label="wagtailadmin", codename="access_admin"
)
advert_permission = Permission.objects.get(
content_type__app_label="tests", codename="change_advert"
)
user.user_permissions.add(admin_permission)
user.user_permissions.add(advert_permission)
self.login(username="basicadmin", password="password")
response = self.client.get(
reverse(
"wagtailsnippets_tests_advert:usage",
args=["1"],
)
)
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, "Welcome to the Wagtail test site!")
self.assertContains(response, "(Private page)")
self.assertContains(response, "<td>Page</td>", html=True)
self.assertContains(response, "<th>Field</th>", html=True)
self.assertNotContains(response, "<th>If you confirm deletion</th>", html=True)
self.assertContains(response, "<li>Advert</li>", html=True)
def test_usage_with_describe_on_delete_cascade(self):
# resave so that usage count gets updated
page = Page.objects.get(pk=2)
page.save()
response = self.client.get(
reverse("wagtailsnippets_tests_advert:usage", args=["1"])
+ "?describe_on_delete=1"
)
self.assertContains(response, "Welcome to the Wagtail test site!")
self.assertContains(response, "<td>Page</td>", html=True)
self.assertNotContains(response, "<th>Field</th>", html=True)
self.assertContains(response, "<th>If you confirm deletion</th>", html=True)
self.assertContains(response, "Advert")
self.assertContains(response, ": the advert placement will also be deleted")
def test_usage_with_describe_on_delete_set_null(self):
# resave so that usage count gets updated
page = EventPage.objects.first()
page.save()
self.assertEqual(page.feed_image.get_usage().count(), 1)
response = self.client.get(
reverse("wagtailimages:image_usage", args=[page.feed_image.id])
+ "?describe_on_delete=1"
)
self.assertContains(response, page.title)
self.assertContains(response, "<td>Event page</td>", html=True)
self.assertNotContains(response, "<th>Field</th>", html=True)
self.assertContains(response, "<th>If you confirm deletion</th>", html=True)
self.assertContains(response, "Feed image")
self.assertContains(response, ": will unset the reference")
def test_usage_with_describe_on_delete_gfk(self):
advert = Advert.objects.get(pk=1)
gfk_page = GenericSnippetPage(
title="Foobar Title",
snippet_content_object=advert,
)
Page.objects.get(pk=1).add_child(instance=gfk_page)
self.assertEqual(ReferenceIndex.get_grouped_references_to(advert).count(), 1)
response = self.client.get(
reverse("wagtailsnippets_tests_advert:usage", args=["1"])
+ "?describe_on_delete=1"
)
self.assertNotContains(response, "Welcome to the Wagtail test site!")
self.assertContains(response, "Foobar Title")
self.assertContains(response, "<td>Generic snippet page</td>", html=True)
self.assertNotContains(response, "<th>Field</th>", html=True)
self.assertContains(response, "<th>If you confirm deletion</th>", html=True)
self.assertContains(response, "Snippet content object")
self.assertContains(response, ": will unset the reference")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,316 @@
from django.contrib.admin.utils import quote
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase, override_settings
from django.urls import reverse
from wagtail.models import Workflow, WorkflowContentType, WorkflowState
from wagtail.test.testapp.models import FullFeaturedSnippet, ModeratedModel
from wagtail.test.utils import WagtailTestUtils
# This module serves to gather snippets-equivalent of workflows-related tests
# that are found throughout page-specific test modules, e.g. test_create_page.py,
# test_edit_page.py, etc.
# The main workflows test modules contain tests for both pages and snippets
# and can be found in:
# - wagtail.tests.test_workflow
# for testing workflow operations through the Workflow model methods
# - wagtail.admin.tests.test_workflows
# for testing workflow operations through views and testing Workflow settings views
class BaseWorkflowsTestCase(WagtailTestUtils, TestCase):
model = FullFeaturedSnippet
def setUp(self):
self.user = self.login()
self.object = self.model.objects.create(text="I'm a full-featured snippet!")
self.object.save_revision().publish()
# Assign default workflow to the snippet model
self.content_type = ContentType.objects.get_for_model(self.model)
self.workflow = Workflow.objects.first()
WorkflowContentType.objects.create(
content_type=self.content_type,
workflow=self.workflow,
)
@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.object.pk)]
return reverse(self.object.snippet_viewset.get_url_name(name), args=args)
class TestCreateView(BaseWorkflowsTestCase):
def get(self):
return self.client.get(self.get_url("add", ()))
def post(self, post_data):
return self.client.post(self.get_url("add", ()), post_data)
def test_get_workflow_buttons_shown(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertContains(
response,
'<button type="submit" name="action-submit" value="Submit to Moderators approval" class="button">',
count=1,
)
@override_settings(WAGTAIL_WORKFLOW_ENABLED=False)
def test_get_workflow_buttons_not_shown_when_workflow_disabled(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, 'name="action-submit"')
def test_post_submit_for_moderation(self):
response = self.post({"text": "Newly created", "action-submit": "Submit"})
object = self.model.objects.get(text="Newly created")
self.assertRedirects(response, self.get_url("list", ()))
self.assertIsInstance(object, self.model)
# The object should be created, but not live
self.assertEqual(object.text, "Newly created")
self.assertFalse(object.live)
self.assertFalse(object.first_published_at)
# The object should now be in moderation
self.assertEqual(
object.current_workflow_state.status,
WorkflowState.STATUS_IN_PROGRESS,
)
# There should be a draft revision with the data
self.assertEqual(object.latest_revision.object_str, "Newly created")
# The current task state should point to the latest revision
self.assertEqual(
object.current_workflow_task_state.revision,
object.latest_revision,
)
class TestCreateViewNotLockable(TestCreateView):
model = ModeratedModel
class TestEditView(BaseWorkflowsTestCase):
def get(self):
return self.client.get(self.get_url("edit"))
def post(self, post_data):
return self.client.post(self.get_url("edit"), post_data)
def test_get_workflow_buttons_shown(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertContains(
response,
'<button type="submit" name="action-submit" value="Submit to Moderators approval" class="button">',
count=1,
)
@override_settings(WAGTAIL_WORKFLOW_ENABLED=False)
def test_get_workflow_buttons_not_shown_when_workflow_disabled(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, 'name="action-submit"')
def test_post_submit_for_moderation(self):
response = self.post({"text": "Edited!", "action-submit": "Submit"})
self.object.refresh_from_db()
self.assertRedirects(response, self.get_url("list", ()))
self.assertIsInstance(self.object, self.model)
# The live instance should still be live and should not be updated
self.assertEqual(self.object.text, "I'm a full-featured snippet!")
self.assertTrue(self.object.live)
self.assertTrue(self.object.first_published_at)
self.assertTrue(self.object.has_unpublished_changes)
# The object should now be in moderation
self.assertEqual(
self.object.current_workflow_state.status,
WorkflowState.STATUS_IN_PROGRESS,
)
# There should be a draft revision with the changes
self.assertEqual(self.object.latest_revision.object_str, "Edited!")
# The current task state should point to the latest revision
self.assertEqual(
self.object.current_workflow_task_state.revision,
self.object.latest_revision,
)
class TestEditViewNotLockable(TestEditView):
model = ModeratedModel
class TestWorkflowHistory(BaseWorkflowsTestCase):
def setUp(self):
super().setUp()
self.object.text = "Edited!"
self.object.save_revision()
self.workflow_state = self.workflow.start(self.object, self.user)
def test_get_index(self):
response = self.client.get(self.get_url("workflow_history"))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(
response, "wagtailadmin/shared/workflow_history/index.html"
)
self.assertContains(response, self.get_url("edit"))
self.assertContains(
response,
self.get_url(
"workflow_history_detail",
(quote(self.object.pk), self.workflow_state.id),
),
)
# Should show the currently in progress workflow
self.assertContains(response, "Moderators approval")
self.assertContains(response, "In progress")
self.assertContains(response, "test@email.com")
def test_get_index_with_bad_permissions(self):
# Remove privileges from user
self.user.is_superuser = False
self.user.user_permissions.add(
Permission.objects.get(
content_type__app_label="wagtailadmin", codename="access_admin"
)
)
self.user.save()
response = self.client.get(self.get_url("workflow_history"))
self.assertRedirects(response, reverse("wagtailadmin_home"))
def test_get_detail(self):
response = self.client.get(
self.get_url(
"workflow_history_detail",
(quote(self.object.pk), self.workflow_state.id),
),
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(
response, "wagtailadmin/shared/workflow_history/detail.html"
)
self.assertContains(response, self.get_url("edit"))
self.assertContains(response, self.get_url("workflow_history"))
self.assertContains(response, '<div class="w-tabs" data-tabs>')
self.assertContains(response, '<div class="tab-content">')
self.assertContains(response, "Tasks")
self.assertContains(response, "Timeline")
# Should show the currently in progress workflow with the latest revision
self.assertContains(response, "Edited!")
self.assertContains(response, "Moderators approval")
self.assertContains(response, "In progress")
self.assertContains(response, "test@email.com")
def test_get_detail_completed(self):
self.workflow_state.current_task_state.approve(user=None)
response = self.client.get(
self.get_url(
"workflow_history_detail",
(quote(self.object.pk), self.workflow_state.id),
),
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(
response, "wagtailadmin/shared/workflow_history/detail.html"
)
self.assertContains(response, self.get_url("edit"))
self.assertContains(response, self.get_url("workflow_history"))
self.assertContains(response, '<div class="w-tabs" data-tabs>')
self.assertContains(response, '<div class="tab-content">')
self.assertContains(response, "Tasks")
self.assertContains(response, "Timeline")
# Should show the completed workflow with the latest revision
self.assertContains(response, "Edited!")
self.assertContains(response, "Moderators approval")
self.assertContains(response, "Workflow completed")
self.assertContains(response, "test@email.com")
self.assertNotContains(response, "In progress")
def test_get_detail_with_bad_permissions(self):
# Remove privileges from user
self.user.is_superuser = False
self.user.user_permissions.add(
Permission.objects.get(
content_type__app_label="wagtailadmin", codename="access_admin"
)
)
self.user.save()
response = self.client.get(
self.get_url(
"workflow_history_detail",
(quote(self.object.pk), self.workflow_state.id),
),
)
self.assertRedirects(response, reverse("wagtailadmin_home"))
def test_get_history_renders_comment(self):
self.workflow_state.current_task_state.reject(comment="Can be better")
# Ensure the comment in the log entry is rendered in the History view.
# This is the main History view and not the Workflow History view, but
# we test it here so we can reuse the workflow setup.
response = self.client.get(self.get_url("history", (quote(self.object.pk),)))
self.assertContains(
response,
"<div>Comment: <em>Can be better</em></div>",
html=True,
)
class TestConfirmWorkflowCancellation(BaseWorkflowsTestCase):
def setUp(self):
super().setUp()
self.object.text = "Edited!"
self.object.save_revision()
self.workflow_state = self.workflow.start(self.object, self.user)
def test_get_confirm_workflow_cancellation(self):
response = self.client.get(self.get_url("confirm_workflow_cancellation"))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(
response, "wagtailadmin/generic/confirm_workflow_cancellation.html"
)
self.assertContains(
response,
"Publishing this full-featured snippet will cancel the current workflow.",
)
self.assertContains(
response, "Would you still like to publish this full-featured snippet?"
)
@override_settings(WAGTAIL_WORKFLOW_CANCEL_ON_PUBLISH=False)
def test_get_confirm_workflow_cancellation_with_disabled_setting(self):
response = self.client.get(self.get_url("confirm_workflow_cancellation"))
self.assertEqual(response.status_code, 200)
self.assertTemplateNotUsed(
response,
"wagtailadmin/generic/confirm_workflow_cancellation.html",
)
self.assertJSONEqual(
response.content.decode(),
{"step": "no_confirmation_needed"},
)