wagtail/wagtail/admin/tests/pages/test_explorer_view.py

1289 wiersze
54 KiB
Python

from django.contrib.auth.models import AbstractBaseUser, Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.core import paginator
from django.test import TestCase, override_settings
from django.urls import reverse
from django.utils.http import urlencode
from wagtail import hooks
from wagtail.admin.widgets import Button
from wagtail.models import GroupPagePermission, Locale, Page, Site, Workflow
from wagtail.test.testapp.models import (
CustomPermissionPage,
SimplePage,
SingleEventPage,
StandardIndex,
)
from wagtail.test.utils import WagtailTestUtils
from wagtail.test.utils.timestamps import local_datetime
from wagtail.utils.deprecation import RemovedInWagtail70Warning
class TestPageExplorer(WagtailTestUtils, TestCase):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
# Add child page
self.child_page = SimplePage(
title="Hello world!",
slug="hello-world",
content="hello",
)
self.root_page.add_child(instance=self.child_page)
# more child pages to test ordering
self.old_page = StandardIndex(
title="Old page",
slug="old-page",
latest_revision_created_at=local_datetime(2010, 1, 1),
)
self.root_page.add_child(instance=self.old_page)
self.new_page = SimplePage(
title="New page",
slug="new-page",
content="hello",
latest_revision_created_at=local_datetime(2016, 1, 1),
)
self.root_page.add_child(instance=self.new_page)
# Login
self.user = self.login()
def assertContainsActiveFilter(self, response, text, param):
soup = self.get_soup(response.content)
active_filter = soup.select_one(".w-active-filters .w-pill__content")
clear_button = soup.select_one(".w-active-filters .w-pill__remove")
self.assertIsNotNone(active_filter)
self.assertEqual(active_filter.get_text(separator=" ", strip=True), text)
self.assertIsNotNone(clear_button)
self.assertNotIn(param, clear_button.attrs.get("data-w-swap-src-value"))
self.assertEqual(clear_button.attrs.get("data-w-swap-reflect-value"), "true")
def test_explore(self):
explore_url = reverse("wagtailadmin_explore", args=(self.root_page.id,))
response = self.client.get(explore_url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/explorable_index.html")
self.assertEqual(self.root_page, response.context["parent_page"])
# child pages should be most recent first
# (with null latest_revision_created_at at the end)
page_ids = [page.id for page in response.context["pages"]]
self.assertEqual(
page_ids, [self.new_page.id, self.old_page.id, self.child_page.id]
)
expected_new_page_copy_url = (
reverse("wagtailadmin_pages:copy", args=(self.new_page.id,))
+ "?"
+ urlencode({"next": explore_url})
)
self.assertContains(response, f'href="{expected_new_page_copy_url}"')
self.assertContains(response, "1-3 of 3")
# Should contain a link to the history view
# one in the header dropdown button, one beside the side panel toggles,
# one in the status side panel
# (root_page is a site root, not the Root page, so it should be shown)
self.assertContains(
response,
reverse("wagtailadmin_pages:history", args=(self.root_page.id,)),
count=3,
)
def test_explore_results(self):
explore_results_url = reverse(
"wagtailadmin_explore_results", args=(self.root_page.id,)
)
response = self.client.get(explore_results_url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/index_results.html")
self.assertEqual(self.root_page, response.context["parent_page"])
page_ids = [page.id for page in response.context["pages"]]
self.assertEqual(
page_ids, [self.new_page.id, self.old_page.id, self.child_page.id]
)
# the 'next' parameter should return to the explore view, NOT
# the partial explore_results view
explore_url = reverse("wagtailadmin_explore", args=(self.root_page.id,))
expected_new_page_copy_url = (
reverse("wagtailadmin_pages:copy", args=(self.new_page.id,))
+ "?"
+ urlencode({"next": explore_url})
)
self.assertContains(response, f'href="{expected_new_page_copy_url}"')
self.assertContains(response, "1-3 of 3")
def test_explore_root(self):
response = self.client.get(reverse("wagtailadmin_explore_root"))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/explorable_index.html")
self.assertEqual(Page.objects.get(id=1), response.context["parent_page"])
self.assertIn(self.root_page, response.context["pages"])
# Should not contain a link to the history view
self.assertNotContains(
response,
reverse("wagtailadmin_pages:history", args=(1,)),
)
def test_explore_root_shows_icon(self):
response = self.client.get(reverse("wagtailadmin_explore_root"))
self.assertEqual(response.status_code, 200)
soup = self.get_soup(response.content)
# Administrator (or user with add_site permission) should see the
# sites link with its icon
url = reverse("wagtailsites:index")
link = soup.select_one(f'td a[href="{url}"]')
self.assertIsNotNone(link)
icon = link.select_one("svg use[href='#icon-site']")
self.assertIsNotNone(icon)
def test_ordering(self):
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
{"ordering": "title"},
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/explorable_index.html")
self.assertEqual(response.context["ordering"], "title")
# child pages should be ordered by title
page_ids = [page.id for page in response.context["pages"]]
self.assertEqual(
page_ids, [self.child_page.id, self.new_page.id, self.old_page.id]
)
def test_ordering_search_results_by_created_at(self):
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
{"q": "page", "ordering": "latest_revision_created_at"},
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/index.html")
# child pages should be ordered by updated_at, oldest first
page_ids = [page.id for page in response.context["pages"]]
self.assertEqual(page_ids, [self.old_page.id, self.new_page.id])
def test_ordering_search_results_by_content_type(self):
# Ordering search results by content_type is not currently supported,
# but should not cause an error
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
{"q": "page", "ordering": "content_type"},
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/index.html")
def test_change_default_child_page_ordering_attribute(self):
# save old get_default_order to reset at end of test
# overriding class methods does not reset at end of test case
default_order = self.root_page.__class__.admin_default_ordering
self.root_page.__class__.admin_default_ordering = "title"
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,))
)
# child pages should be ordered by title
page_ids = [page.id for page in response.context["pages"]]
self.assertEqual(
page_ids, [self.child_page.id, self.new_page.id, self.old_page.id]
)
self.assertEqual("title", self.root_page.get_admin_default_ordering())
self.assertEqual(response.context["ordering"], "title")
# reset default order at the end of the test
self.root_page.__class__.admin_default_ordering = default_order
def test_change_default_child_page_ordering_method(self):
# save old get_default_order to reset at end of test
# overriding class methods does not reset at end of test case
default_order_function = self.root_page.__class__.get_admin_default_ordering
def get_default_order(obj):
return "-title"
# override get_default_order_method
self.root_page.__class__.get_admin_default_ordering = get_default_order
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,))
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/explorable_index.html")
# child pages should be ordered by title
page_ids = [page.id for page in response.context["pages"]]
self.assertEqual("-title", self.root_page.get_admin_default_ordering())
self.assertEqual(
page_ids, [self.old_page.id, self.new_page.id, self.child_page.id]
)
self.assertEqual(response.context["ordering"], "-title")
# reset default order function at the end of the test
self.root_page.__class__.get_admin_default_ordering = default_order_function
def test_reverse_ordering(self):
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
{"ordering": "-title"},
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/explorable_index.html")
self.assertEqual(response.context["ordering"], "-title")
# child pages should be ordered by title
page_ids = [page.id for page in response.context["pages"]]
self.assertEqual(
page_ids, [self.old_page.id, self.new_page.id, self.child_page.id]
)
def test_ordering_by_last_revision_forward(self):
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
{"ordering": "latest_revision_created_at"},
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/explorable_index.html")
self.assertEqual(response.context["ordering"], "latest_revision_created_at")
# child pages should be oldest revision first
# (with null latest_revision_created_at at the start)
page_ids = [page.id for page in response.context["pages"]]
self.assertEqual(
page_ids, [self.child_page.id, self.old_page.id, self.new_page.id]
)
def test_invalid_ordering(self):
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
{"ordering": "invalid_order"},
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/explorable_index.html")
self.assertEqual(response.context["ordering"], "-latest_revision_created_at")
def test_reordering(self):
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
{"ordering": "ord"},
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/explorable_index.html")
self.assertEqual(response.context["ordering"], "ord")
# child pages should be ordered by native tree order (i.e. by creation time)
page_ids = [page.id for page in response.context["pages"]]
self.assertEqual(
page_ids, [self.child_page.id, self.old_page.id, self.new_page.id]
)
# Pages must not be paginated
self.assertNotIsInstance(response.context["pages"], paginator.Page)
def test_construct_explorer_page_queryset_hook(self):
# testapp implements a construct_explorer_page_queryset hook
# that only returns pages with a slug starting with 'hello'
# when the 'polite_pages_only' URL parameter is set
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
{"polite_pages_only": "yes_please"},
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/explorable_index.html")
page_ids = [page.id for page in response.context["pages"]]
self.assertEqual(page_ids, [self.child_page.id])
def test_construct_explorer_page_queryset_hook_with_ordering(self):
def set_custom_ordering(parent_page, pages, request):
return pages.order_by("-title")
with hooks.register_temporarily(
"construct_explorer_page_queryset", set_custom_ordering
):
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,))
)
# child pages should be ordered by according to the hook preference
page_ids = [page.id for page in response.context["pages"]]
self.assertEqual(
page_ids, [self.old_page.id, self.new_page.id, self.child_page.id]
)
def test_construct_page_listing_buttons_hook_with_old_signature(self):
def add_dummy_button(buttons, page, page_perms, context=None):
item = Button(
label="Dummy Button",
url="/dummy-button",
priority=10,
)
buttons.append(item)
with hooks.register_temporarily(
"construct_page_listing_buttons", add_dummy_button
):
with self.assertWarnsMessage(
RemovedInWagtail70Warning,
"`construct_page_listing_buttons` hook functions should accept a `user` argument instead of `page_perms`",
):
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,))
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/explorable_index.html")
self.assertContains(response, "Dummy Button")
self.assertContains(response, "/dummy-button")
def test_construct_page_listing_buttons_hook_with_new_signature(self):
def add_dummy_button(buttons, page, user, context=None):
if not isinstance(user, AbstractBaseUser):
raise TypeError("expected a user instance")
item = Button(
label="Dummy Button",
url="/dummy-button",
priority=10,
)
buttons.append(item)
with hooks.register_temporarily(
"construct_page_listing_buttons", add_dummy_button
):
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,))
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/explorable_index.html")
self.assertContains(response, "Dummy Button")
self.assertContains(response, "/dummy-button")
def make_pages(self):
for i in range(150):
self.root_page.add_child(
instance=SimplePage(
title="Page " + str(i),
slug="page-" + str(i),
content="hello",
)
)
def test_pagination(self):
self.make_pages()
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)), {"p": 2}
)
# Check response
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/explorable_index.html")
# Check that we got the correct page
self.assertEqual(response.context["page_obj"].number, 2)
self.assertContains(response, "51-100 of 153")
def test_pagination_invalid(self):
self.make_pages()
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
{"p": "Hello World!"},
)
# Check response
self.assertEqual(response.status_code, 404)
def test_pagination_out_of_range(self):
self.make_pages()
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)), {"p": 99999}
)
# Check response
self.assertEqual(response.status_code, 404)
def test_no_pagination_with_custom_ordering(self):
self.make_pages()
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
{"ordering": "ord"},
)
# Check response
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/index.html")
# Check that we don't have a paginator page object
self.assertIsNone(response.context["page_obj"])
# Check that all pages are shown
self.assertContains(response, "1-153 of 153")
self.assertEqual(len(response.context["pages"]), 153)
@override_settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=True)
def test_no_thousand_separators_in_bulk_action_checkbox(self):
"""
Test that the USE_THOUSAND_SEPARATOR setting does mess up object IDs in
bulk actions checkboxes
"""
self.root_page.add_child(
instance=SimplePage(
pk=1000,
title="Page 1000",
slug="page-1000",
content="hello",
)
)
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,))
)
expected = 'data-object-id="1000"'
self.assertContains(response, expected)
def test_listing_uses_specific_models(self):
# SingleEventPage has custom URL routing; the 'live' link in the listing
# should show the custom URL, which requires us to use the specific version
# of the class
self.new_event = SingleEventPage(
title="New event",
location="the moon",
audience="public",
cost="free",
date_from="2001-01-01",
latest_revision_created_at=local_datetime(2016, 1, 1),
)
self.root_page.add_child(instance=self.new_event)
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,))
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "/new-event/pointless-suffix/")
def make_event_pages(self, count):
for i in range(count):
self.root_page.add_child(
instance=SingleEventPage(
title="New event " + str(i),
location="the moon",
audience="public",
cost="free",
date_from="2001-01-01",
latest_revision_created_at=local_datetime(2016, 1, 1),
)
)
def test_exploring_uses_specific_page_with_custom_display_title(self):
# SingleEventPage has a custom get_admin_display_title method; explorer should
# show the custom title rather than the basic database one
self.make_event_pages(count=1)
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,))
)
self.assertContains(response, "New event 0 (single event)")
new_event = SingleEventPage.objects.latest("pk")
response = self.client.get(
reverse("wagtailadmin_explore", args=(new_event.id,))
)
self.assertContains(response, "New event 0 (single event)")
def test_parent_page_is_specific(self):
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.child_page.id,))
)
self.assertEqual(response.status_code, 200)
self.assertIsInstance(response.context["parent_page"], SimplePage)
def test_explorer_no_perms(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()
admin = reverse("wagtailadmin_home")
self.assertRedirects(
self.client.get(reverse("wagtailadmin_explore", args=(self.root_page.id,))),
admin,
)
self.assertRedirects(
self.client.get(reverse("wagtailadmin_explore_root")), admin
)
def test_explore_with_missing_page_model(self):
# Create a ContentType that doesn't correspond to a real model
missing_page_content_type = ContentType.objects.create(
app_label="tests", model="missingpage"
)
# Turn /home/old-page/ into this content type
Page.objects.filter(id=self.old_page.id).update(
content_type=missing_page_content_type
)
# try to browse the listing that contains the missing model
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,))
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/explorable_index.html")
# try to browse into the page itself
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.old_page.id,))
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/explorable_index.html")
def test_search(self):
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
{"q": "old"},
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/explorable_index.html")
page_ids = [page.id for page in response.context["pages"]]
self.assertEqual(page_ids, [self.old_page.id])
self.assertContains(response, "Search the whole site")
def test_search_results(self):
response = self.client.get(
reverse("wagtailadmin_explore_results", args=(self.root_page.id,)),
{"q": "old"},
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailadmin/pages/index_results.html")
page_ids = [page.id for page in response.context["pages"]]
self.assertEqual(page_ids, [self.old_page.id])
self.assertContains(response, "1-1 of 1")
def test_search_searches_descendants(self):
response = self.client.get(reverse("wagtailadmin_explore_root"), {"q": "old"})
self.assertEqual(response.status_code, 200)
page_ids = [page.id for page in response.context["pages"]]
self.assertEqual(page_ids, [self.old_page.id])
# Results that are not immediate children of the current page should show their parent
self.assertContains(
response,
'<a href="/admin/pages/2/"><svg class="icon icon-arrow-right default" aria-hidden="true"><use href="#icon-arrow-right"></use></svg>Welcome to your new Wagtail site!</a>',
html=True,
)
# search results should not include pages outside parent_page's descendants
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.new_page.id,)),
{"q": "old"},
)
self.assertEqual(response.status_code, 200)
page_ids = [page.id for page in response.context["pages"]]
self.assertEqual(page_ids, [])
def test_search_whole_tree(self):
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.new_page.id,)),
{"q": "old", "search_all": "1"},
)
self.assertEqual(response.status_code, 200)
page_ids = [page.id for page in response.context["pages"]]
self.assertEqual(page_ids, [self.old_page.id])
self.assertContains(
response,
"Search in '<span class=\"w-title-ellipsis\">New page (simple page)</span>'",
)
def test_filter_by_page_type(self):
new_page_child = SimplePage(
title="New page child", slug="new-page-child", content="new page child"
)
self.new_page.add_child(instance=new_page_child)
page_type_pk = ContentType.objects.get_for_model(SimplePage).pk
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
{"content_type": page_type_pk},
)
self.assertEqual(response.status_code, 200)
page_ids = {page.id for page in response.context["pages"]}
self.assertEqual(
page_ids, {self.child_page.id, self.new_page.id, new_page_child.id}
)
self.assertContainsActiveFilter(
response,
"Page type: Simple page",
f"content_type={page_type_pk}",
)
# "Page" should not be listed as a content type
soup = self.get_soup(response.content)
page_type_labels = {
list(label.children)[-1].strip()
for label in soup.select("#id_content_type label")
}
self.assertIn("Simple page", page_type_labels)
self.assertNotIn("Page", page_type_labels)
def test_filter_by_date_updated(self):
new_page_child = SimplePage(
title="New page child",
slug="new-page-child",
content="new page child",
latest_revision_created_at=local_datetime(2016, 1, 1),
)
self.new_page.add_child(instance=new_page_child)
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
{"latest_revision_created_at_from": "2015-01-01"},
)
self.assertEqual(response.status_code, 200)
page_ids = {page.id for page in response.context["pages"]}
self.assertEqual(page_ids, {self.new_page.id, new_page_child.id})
self.assertContainsActiveFilter(
response,
"Date updated: Jan. 1, 2015 -",
"latest_revision_created_at_from=2015-01-01",
)
def test_filter_by_owner(self):
barry = self.create_user(
"barry", password="password", first_name="Barry", last_name="Manilow"
)
self.create_user(
"larry", password="password", first_name="Larry", last_name="King"
)
new_page_child = SimplePage(
title="New page child",
slug="new-page-child",
content="new page child",
owner=barry,
)
self.new_page.add_child(instance=new_page_child)
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
)
self.assertEqual(response.status_code, 200)
# Only users who own any pages should be listed in the filter
self.assertContains(response, "Barry Manilow")
self.assertNotContains(response, "Larry King")
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
{"owner": barry.pk},
)
self.assertEqual(response.status_code, 200)
page_ids = {page.id for page in response.context["pages"]}
self.assertEqual(page_ids, {new_page_child.id})
self.assertContainsActiveFilter(
response,
"Owner: Barry Manilow",
f"owner={barry.pk}",
)
def test_filter_by_edited_by_user(self):
barry = self.create_superuser(
"barry", password="password", first_name="Barry", last_name="Manilow"
)
self.create_user(
"larry", password="password", first_name="Larry", last_name="King"
)
self.login(username="barry", password="password")
post_data = {
"title": "Hello world!",
"content": "hello from Barry",
"slug": "hello-world",
}
response = self.client.post(
reverse("wagtailadmin_pages:edit", args=(self.child_page.id,)), post_data
)
self.assertEqual(response.status_code, 302)
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
{"edited_by": barry.pk},
)
self.assertEqual(response.status_code, 200)
# Only users who have edited any pages should be listed in the filter
self.assertContains(response, "Barry Manilow")
self.assertNotContains(response, "Larry King")
page_ids = {page.id for page in response.context["pages"]}
self.assertEqual(page_ids, {self.child_page.id})
self.assertContainsActiveFilter(
response,
"Edited by: Barry Manilow",
f"edited_by={barry.pk}",
)
def test_filter_by_site(self):
new_site = Site.objects.create(
hostname="new.example.com", root_page=self.new_page
)
new_page_child = SimplePage(
title="New page child",
slug="new-page-child",
content="new page child",
)
self.new_page.add_child(instance=new_page_child)
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
{"site": new_site.pk},
)
self.assertEqual(response.status_code, 200)
page_ids = {page.id for page in response.context["pages"]}
self.assertEqual(page_ids, {self.new_page.id, new_page_child.id})
self.assertContainsActiveFilter(
response,
"Site: new.example.com",
f"site={new_site.pk}",
)
def test_filter_by_has_child_pages(self):
new_page_child = SimplePage(
title="New page child",
slug="new-page-child",
content="new page child",
)
self.new_page.add_child(instance=new_page_child)
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
{"has_child_pages": "true"},
)
self.assertEqual(response.status_code, 200)
page_ids = {page.id for page in response.context["pages"]}
self.assertEqual(page_ids, {self.new_page.id})
self.assertContainsActiveFilter(
response,
"Has child pages: Yes",
"has_child_pages=true",
)
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
{"has_child_pages": "false"},
)
self.assertEqual(response.status_code, 200)
page_ids = {page.id for page in response.context["pages"]}
self.assertEqual(
page_ids, {self.child_page.id, self.old_page.id, new_page_child.id}
)
self.assertContainsActiveFilter(
response,
"Has child pages: No",
"has_child_pages=false",
)
def test_invalid_filter(self):
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.root_page.id,)),
{"has_child_pages": "unknown"},
)
self.assertEqual(response.status_code, 200)
soup = self.get_soup(response.content)
active_filters = soup.select_one(".w-active-filters")
self.assertIsNone(active_filters)
error_message = soup.select_one(".w-field__errors .error-message")
self.assertIsNotNone(error_message)
self.assertEqual(
error_message.string.strip(),
"Select a valid choice. unknown is not one of the available choices.",
)
def test_explore_custom_permissions(self):
page = CustomPermissionPage(title="Page with custom perms", slug="custom-perms")
self.root_page.add_child(instance=page)
response = self.client.get(reverse("wagtailadmin_explore", args=(page.id,)))
self.assertEqual(response.status_code, 200)
# Respecting PagePermissionTester.can_view_revisions(),
# should not contain a link to the history view
self.assertNotContains(
response,
reverse("wagtailadmin_pages:history", args=(page.id,)),
)
class TestBreadcrumb(WagtailTestUtils, TestCase):
fixtures = ["test.json"]
def test_breadcrumb_next_present(self):
self.user = self.login()
# get the explorer view for a subpage of a SimplePage
page = Page.objects.get(url_path="/home/secret-plans/steal-underpants/")
response = self.client.get(reverse("wagtailadmin_explore", args=(page.id,)))
self.assertEqual(response.status_code, 200)
# The breadcrumbs controller identifier should be present
self.assertContains(response, 'data-controller="w-breadcrumbs"')
def test_breadcrumb_uses_specific_titles(self):
self.user = self.login()
# get the explorer view for a subpage of a SimplePage
page = Page.objects.get(url_path="/home/secret-plans/steal-underpants/")
response = self.client.get(reverse("wagtailadmin_explore", args=(page.id,)))
# The breadcrumb should pick up SimplePage's overridden get_admin_display_title method
expected_url = reverse(
"wagtailadmin_explore",
args=(Page.objects.get(url_path="/home/secret-plans/").id,),
)
expected = (
"""
<li class="w-h-full w-flex w-items-center w-overflow-hidden w-transition w-duration-300 w-whitespace-nowrap w-flex-shrink-0 w-max-w-0" data-w-breadcrumbs-target="content" hidden>
<a class="w-flex w-items-center w-text-text-label w-pr-0.5 w-text-14 w-no-underline w-outline-offset-inside w-border-b w-border-b-2 w-border-transparent w-box-content hover:w-border-current hover:w-text-text-label" href="%s">
Secret plans (simple page)
</a>
<svg class="icon icon-arrow-right w-w-4 w-h-4 w-ml-3" aria-hidden="true">
<use href="#icon-arrow-right"></use>
</svg>
</li>
"""
% expected_url
)
self.assertContains(response, expected, html=True)
class TestPageExplorerSidePanel(WagtailTestUtils, TestCase):
fixtures = ["test.json"]
def test_side_panel_present(self):
self.user = self.login()
# get the explorer view for a subpage of a SimplePage
page = Page.objects.get(url_path="/home/secret-plans/steal-underpants/")
response = self.client.get(reverse("wagtailadmin_explore", args=(page.id,)))
self.assertEqual(response.status_code, 200)
# The side panel should be present with data-form-side-explorer attribute
html = response.content.decode()
self.assertTagInHTML(
"<aside data-form-side data-form-side-explorer>",
html,
allow_extra_attrs=True,
)
class TestPageExplorerSignposting(WagtailTestUtils, TestCase):
fixtures = ["test.json"]
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=1)
# Find page with an associated site
self.site_page = Page.objects.get(id=2)
# Add another top-level page (which will have no corresponding site record)
self.no_site_page = SimplePage(
title="Hello world!",
slug="hello-world",
content="hello",
)
self.root_page.add_child(instance=self.no_site_page)
# Tests for users that have both add-site permission, and explore permission at the given view;
# warning messages should include advice re configuring sites
def test_admin_at_root(self):
self.login(username="superuser", password="password")
response = self.client.get(reverse("wagtailadmin_explore_root"))
self.assertEqual(response.status_code, 200)
# Administrator (or user with add_site permission) should get the full message
# about configuring sites
self.assertContains(
response,
(
"The root level is where you can add new sites to your Wagtail installation. "
"Pages created here will not be accessible at any URL until they are associated with a site."
),
)
self.assertContains(
response, """<a href="/admin/sites/">Configure a site now.</a>"""
)
def test_admin_at_non_site_page(self):
self.login(username="superuser", password="password")
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.no_site_page.id,))
)
self.assertEqual(response.status_code, 200)
# Administrator (or user with add_site permission) should get a warning about
# unroutable pages, and be directed to the site config area
self.assertContains(
response,
(
"There is no site set up for this location. "
"Pages created here will not be accessible at any URL until a site is associated with this location."
),
)
self.assertContains(
response, """<a href="/admin/sites/">Configure a site now.</a>"""
)
def test_admin_at_site_page(self):
self.login(username="superuser", password="password")
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.site_page.id,))
)
self.assertEqual(response.status_code, 200)
# There should be no warning message here
self.assertNotContains(response, "Pages created here will not be accessible")
# Tests for standard users that have explore permission at the given view;
# warning messages should omit advice re configuring sites
def test_nonadmin_at_root(self):
# Assign siteeditor permission over no_site_page, so that the deepest-common-ancestor
# logic allows them to explore root
GroupPagePermission.objects.create(
group=Group.objects.get(name="Site-wide editors"),
page=self.no_site_page,
permission_type="add",
)
self.login(username="siteeditor", password="password")
response = self.client.get(reverse("wagtailadmin_explore_root"))
self.assertEqual(response.status_code, 200)
# Non-admin should get a simple "create pages as children of the homepage" prompt
self.assertContains(
response,
"Pages created here will not be accessible at any URL. "
"To add pages to an existing site, create them as children of the homepage.",
)
def test_nonadmin_at_non_site_page(self):
# Assign siteeditor permission over no_site_page
GroupPagePermission.objects.create(
group=Group.objects.get(name="Site-wide editors"),
page=self.no_site_page,
permission_type="add",
)
self.login(username="siteeditor", password="password")
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.no_site_page.id,))
)
self.assertEqual(response.status_code, 200)
# Non-admin should get a warning about unroutable pages
self.assertContains(
response,
(
"There is no site record for this location. "
"Pages created here will not be accessible at any URL."
),
)
def test_nonadmin_at_site_page(self):
self.login(username="siteeditor", password="password")
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.site_page.id,))
)
self.assertEqual(response.status_code, 200)
# There should be no warning message here
self.assertNotContains(response, "Pages created here will not be accessible")
# Tests for users that have explore permission *somewhere*, but not at the view being tested;
# in all cases, they should be redirected to their explorable root
def test_bad_permissions_at_root(self):
# 'siteeditor' does not have permission to explore the root
self.login(username="siteeditor", password="password")
response = self.client.get(reverse("wagtailadmin_explore_root"))
# Users without permission to explore here should be redirected to their explorable root.
self.assertEqual(
(response.status_code, response["Location"]),
(302, reverse("wagtailadmin_explore", args=(self.site_page.pk,))),
)
def test_bad_permissions_at_non_site_page(self):
# 'siteeditor' does not have permission to explore no_site_page
self.login(username="siteeditor", password="password")
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.no_site_page.id,))
)
# Users without permission to explore here should be redirected to their explorable root.
self.assertEqual(
(response.status_code, response["Location"]),
(302, reverse("wagtailadmin_explore", args=(self.site_page.pk,))),
)
def test_bad_permissions_at_site_page(self):
# Adjust siteeditor's permission so that they have permission over no_site_page
# instead of site_page
Group.objects.get(name="Site-wide editors").page_permissions.update(
page_id=self.no_site_page.id
)
self.login(username="siteeditor", password="password")
response = self.client.get(
reverse("wagtailadmin_explore", args=(self.site_page.id,))
)
# Users without permission to explore here should be redirected to their explorable root.
self.assertEqual(
(response.status_code, response["Location"]),
(302, reverse("wagtailadmin_explore", args=(self.no_site_page.pk,))),
)
class TestExplorablePageVisibility(WagtailTestUtils, TestCase):
"""
Test the way that the Explorable Pages functionality manifests within the Explorer.
This is isolated in its own test case because it requires a custom page tree and custom set of
users and groups.
The fixture sets up this page tree:
========================================================
ID Site Path
========================================================
1 /
2 testserver /home/
3 testserver /home/about-us/
4 example.com /example-home/
5 example.com /example-home/content/
6 example.com /example-home/content/page-1/
7 example.com /example-home/content/page-2/
9 example.com /example-home/content/page-2/child-1
8 example.com /example-home/other-content/
10 example2.com /home-2/
========================================================
Group 1 has explore and choose permissions rooted at testserver's homepage.
Group 2 has explore and choose permissions rooted at example.com's page-1.
Group 3 has explore and choose permissions rooted at example.com's other-content.
User "jane" is in Group 1.
User "bob" is in Group 2.
User "sam" is in Groups 1 and 2.
User "josh" is in Groups 2 and 3.
User "mary" is is no Groups, but she has the "access wagtail admin" permission.
User "superman" is an admin.
"""
fixtures = ["test_explorable_pages.json"]
# Integration tests adapted from @coredumperror
def test_admin_can_explore_every_page(self):
self.login(username="superman", password="password")
for page in Page.objects.all():
response = self.client.get(reverse("wagtailadmin_explore", args=[page.pk]))
self.assertEqual(response.status_code, 200)
def test_admin_sees_root_page_as_explorer_root(self):
self.login(username="superman", password="password")
response = self.client.get(reverse("wagtailadmin_explore_root"))
self.assertEqual(response.status_code, 200)
# Administrator should see the full list of children of the Root page.
self.assertContains(response, "Welcome to testserver!")
self.assertContains(response, "Welcome to example.com!")
def test_admin_sees_breadcrumbs_up_to_root_page(self):
self.login(username="superman", password="password")
response = self.client.get(reverse("wagtailadmin_explore", args=[6]))
self.assertEqual(response.status_code, 200)
expected = """
<li class="w-h-full w-flex w-items-center w-overflow-hidden w-transition w-duration-300 w-whitespace-nowrap w-flex-shrink-0 w-max-w-0" data-w-breadcrumbs-target="content" hidden>
<a class="w-flex w-items-center w-text-text-label w-pr-0.5 w-text-14 w-no-underline w-outline-offset-inside w-border-b w-border-b-2 w-border-transparent w-box-content hover:w-border-current hover:w-text-text-label" href="/admin/pages/">
Root
</a>
<svg class="icon icon-arrow-right w-w-4 w-h-4 w-ml-3" aria-hidden="true">
<use href="#icon-arrow-right"></use>
</svg>
</li>
"""
self.assertContains(response, expected, html=True)
expected = """
<li class="w-h-full w-flex w-items-center w-overflow-hidden w-transition w-duration-300 w-whitespace-nowrap w-flex-shrink-0 w-max-w-0" data-w-breadcrumbs-target="content" hidden>
<a class="w-flex w-items-center w-text-text-label w-pr-0.5 w-text-14 w-no-underline w-outline-offset-inside w-border-b w-border-b-2 w-border-transparent w-box-content hover:w-border-current hover:w-text-text-label" href="/admin/pages/4/">
Welcome to example.com!
</a>
<svg class="icon icon-arrow-right w-w-4 w-h-4 w-ml-3" aria-hidden="true">
<use href="#icon-arrow-right"></use>
</svg>
</li>
"""
self.assertContains(response, expected, html=True)
expected = """
<li class="w-h-full w-flex w-items-center w-overflow-hidden w-transition w-duration-300 w-whitespace-nowrap w-flex-shrink-0 w-max-w-0" data-w-breadcrumbs-target="content" hidden>
<a class="w-flex w-items-center w-text-text-label w-pr-0.5 w-text-14 w-no-underline w-outline-offset-inside w-border-b w-border-b-2 w-border-transparent w-box-content hover:w-border-current hover:w-text-text-label" href="/admin/pages/5/">
Content
</a>
<svg class="icon icon-arrow-right w-w-4 w-h-4 w-ml-3" aria-hidden="true">
<use href="#icon-arrow-right"></use>
</svg>
</li>
"""
self.assertContains(response, expected, html=True)
def test_nonadmin_sees_breadcrumbs_up_to_cca(self):
self.login(username="josh", password="password")
response = self.client.get(reverse("wagtailadmin_explore", args=[6]))
self.assertEqual(response.status_code, 200)
# While at "Page 1", Josh should see the breadcrumbs leading only as far back as the example.com homepage,
# since it's his Closest Common Ancestor.
expected = """
<li class="w-h-full w-flex w-items-center w-overflow-hidden w-transition w-duration-300 w-whitespace-nowrap w-flex-shrink-0 w-max-w-0" data-w-breadcrumbs-target="content" hidden>
<a class="w-flex w-items-center w-text-text-label w-pr-0.5 w-text-14 w-no-underline w-outline-offset-inside w-border-b w-border-b-2 w-border-transparent w-box-content hover:w-border-current hover:w-text-text-label" href="/admin/pages/4/">
Welcome to example.com!
</a>
<svg class="icon icon-arrow-right w-w-4 w-h-4 w-ml-3" aria-hidden="true">
<use href="#icon-arrow-right"></use>
</svg>
</li>
"""
self.assertContains(response, expected, html=True)
expected = """
<li class="w-h-full w-flex w-items-center w-overflow-hidden w-transition w-duration-300 w-whitespace-nowrap w-flex-shrink-0 w-max-w-0" data-w-breadcrumbs-target="content" hidden>
<a class="w-flex w-items-center w-text-text-label w-pr-0.5 w-text-14 w-no-underline w-outline-offset-inside w-border-b w-border-b-2 w-border-transparent w-box-content hover:w-border-current hover:w-text-text-label" href="/admin/pages/5/">
Content
</a>
<svg class="icon icon-arrow-right w-w-4 w-h-4 w-ml-3" aria-hidden="true">
<use href="#icon-arrow-right"></use>
</svg>
</li>
"""
self.assertContains(response, expected, html=True)
def test_nonadmin_sees_non_hidden_root(self):
self.login(username="josh", password="password")
response = self.client.get(reverse("wagtailadmin_explore", args=[4]))
self.assertEqual(response.status_code, 200)
# When Josh is viewing his visible root page, he should the page title as a non-hidden, single-item breadcrumb.
expected = """
<li
class="w-h-full w-flex w-items-center w-overflow-hidden w-transition w-duration-300 w-whitespace-nowrap w-flex-shrink-0 w-font-bold" data-w-breadcrumbs-target="content">
<a class="w-flex w-items-center w-text-text-label w-pr-0.5 w-text-14 w-no-underline w-outline-offset-inside w-border-b w-border-b-2 w-border-transparent w-box-content hover:w-border-current hover:w-text-text-label"
href="/admin/pages/4/">
Welcome to example.com!
</a>
</li>
"""
self.assertContains(response, expected, html=True)
def test_admin_home_page_changes_with_permissions(self):
self.login(username="bob", password="password")
response = self.client.get(reverse("wagtailadmin_home"))
self.assertEqual(response.status_code, 200)
# Bob should only see the welcome for example.com, not testserver
self.assertContains(response, "Welcome to the example.com Wagtail CMS")
self.assertNotContains(response, "testserver")
def test_breadcrumb_with_no_user_permissions(self):
self.login(username="mary", password="password")
response = self.client.get(reverse("wagtailadmin_home"))
self.assertEqual(response.status_code, 200)
# Since Mary has no page permissions, she should not see the breadcrumb
self.assertNotContains(
response,
"""<li class="home breadcrumb-item"><a class="breadcrumb-link" href="/admin/pages/4/" class="icon icon-home text-replace">Home</a></li>""",
)
@override_settings(WAGTAIL_I18N_ENABLED=True)
class TestLocaleSelector(WagtailTestUtils, TestCase):
fixtures = ["test.json"]
def setUp(self):
self.events_page = Page.objects.get(url_path="/home/events/")
self.fr_locale = Locale.objects.create(language_code="fr")
self.translated_events_page = self.events_page.copy_for_translation(
self.fr_locale, copy_parents=True
)
self.user = self.login()
def test_locale_selector(self):
response = self.client.get(
reverse("wagtailadmin_explore", args=[self.events_page.id])
)
html = response.content.decode()
self.assertContains(response, 'id="status-sidebar-english"')
self.assertContains(response, "Switch locales")
add_translation_url = reverse(
"wagtailadmin_explore", args=[self.translated_events_page.id]
)
self.assertTagInHTML(
f'<a href="{add_translation_url}" lang="fr">French</a>',
html,
allow_extra_attrs=True,
)
@override_settings(WAGTAIL_I18N_ENABLED=False)
def test_locale_selector_not_present_when_i18n_disabled(self):
response = self.client.get(
reverse("wagtailadmin_explore", args=[self.events_page.id])
)
html = response.content.decode()
self.assertNotContains(response, "Switch locales")
add_translation_url = reverse(
"wagtailadmin_explore", args=[self.translated_events_page.id]
)
self.assertTagInHTML(
f'<a href="{add_translation_url}" lang="fr">French</a>',
html,
allow_extra_attrs=True,
count=0,
)
class TestInWorkflowStatus(WagtailTestUtils, TestCase):
fixtures = ["test.json"]
@classmethod
def setUpTestData(cls):
cls.event_index = Page.objects.get(url_path="/home/events/")
cls.christmas = Page.objects.get(url_path="/home/events/christmas/").specific
cls.saint_patrick = Page.objects.get(
url_path="/home/events/saint-patrick/"
).specific
cls.christmas.save_revision()
cls.saint_patrick.save_revision()
cls.url = reverse("wagtailadmin_explore", args=[cls.event_index.pk])
def setUp(self):
self.user = self.login()
def test_in_workflow_status(self):
workflow = Workflow.objects.first()
workflow.start(self.christmas, self.user)
workflow.start(self.saint_patrick, self.user)
# Warm up cache
self.client.get(self.url)
with self.assertNumQueries(47):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
soup = self.get_soup(response.content)
for page in [self.christmas, self.saint_patrick]:
status = soup.select_one(f'a.w-status[href="{page.url}"]')
self.assertIsNotNone(status)
self.assertEqual(
status.text.strip(), "Current page status: live + in moderation"
)
self.assertEqual(page.status_string, "live + in moderation")