427 lines
15 KiB
Python
427 lines
15 KiB
Python
|
|
from django.test import RequestFactory, TestCase
|
||
|
|
from django.urls import reverse
|
||
|
|
from django.utils import translation
|
||
|
|
|
||
|
|
from wagtail import hooks
|
||
|
|
from wagtail.admin.menu import (
|
||
|
|
AdminOnlyMenuItem,
|
||
|
|
DismissibleMenuItem,
|
||
|
|
DismissibleSubmenuMenuItem,
|
||
|
|
Menu,
|
||
|
|
MenuItem,
|
||
|
|
SubmenuMenuItem,
|
||
|
|
admin_menu,
|
||
|
|
)
|
||
|
|
from wagtail.admin.ui import sidebar
|
||
|
|
from wagtail.test.utils import WagtailTestUtils
|
||
|
|
from wagtail.users.models import UserProfile
|
||
|
|
|
||
|
|
|
||
|
|
def menu_item_hook(*args, cls=MenuItem, **kwargs):
|
||
|
|
def hook_fn():
|
||
|
|
return cls(*args, **kwargs)
|
||
|
|
|
||
|
|
return hook_fn
|
||
|
|
|
||
|
|
|
||
|
|
class TestMenuRendering(WagtailTestUtils, TestCase):
|
||
|
|
def setUp(self):
|
||
|
|
self.request = RequestFactory().get("/admin")
|
||
|
|
self.request.user = self.create_superuser(username="admin")
|
||
|
|
self.profile = UserProfile.get_for_user(self.request.user)
|
||
|
|
self.user = self.login()
|
||
|
|
|
||
|
|
def test_remember_collapsed(self):
|
||
|
|
"""Sidebar should render with collapsed class applied."""
|
||
|
|
# Sidebar should not be collapsed
|
||
|
|
self.client.cookies["wagtail_sidebar_collapsed"] = "0"
|
||
|
|
response = self.client.get(reverse("wagtailadmin_home"))
|
||
|
|
self.assertNotContains(response, "sidebar-collapsed")
|
||
|
|
|
||
|
|
# Sidebar should be collapsed
|
||
|
|
self.client.cookies["wagtail_sidebar_collapsed"] = "1"
|
||
|
|
response = self.client.get(reverse("wagtailadmin_home"))
|
||
|
|
self.assertContains(response, "sidebar-collapsed")
|
||
|
|
|
||
|
|
def test_simple_menu(self):
|
||
|
|
# Note: initialise the menu before registering hooks as this is what happens in reality.
|
||
|
|
# (the real menus are initialised at the module level in admin/menu.py)
|
||
|
|
menu = Menu(register_hook_name="register_menu_item")
|
||
|
|
|
||
|
|
with hooks.register_temporarily(
|
||
|
|
[
|
||
|
|
("register_menu_item", menu_item_hook("Pages", "/pages/")),
|
||
|
|
("register_menu_item", menu_item_hook("Images", "/images/")),
|
||
|
|
]
|
||
|
|
):
|
||
|
|
rendered = menu.render_component(self.request)
|
||
|
|
|
||
|
|
self.assertIsInstance(rendered, list)
|
||
|
|
self.assertListEqual(
|
||
|
|
rendered,
|
||
|
|
[
|
||
|
|
sidebar.LinkMenuItem("pages", "Pages", "/pages/"),
|
||
|
|
sidebar.LinkMenuItem("images", "Images", "/images/"),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_menu_with_construct_hook(self):
|
||
|
|
menu = Menu(
|
||
|
|
register_hook_name="register_menu_item",
|
||
|
|
construct_hook_name="construct_menu",
|
||
|
|
)
|
||
|
|
|
||
|
|
def remove_images(request, items):
|
||
|
|
items[:] = [item for item in items if not item.name == "images"]
|
||
|
|
|
||
|
|
with hooks.register_temporarily(
|
||
|
|
[
|
||
|
|
("register_menu_item", menu_item_hook("Pages", "/pages/")),
|
||
|
|
("register_menu_item", menu_item_hook("Images", "/images/")),
|
||
|
|
("construct_menu", remove_images),
|
||
|
|
]
|
||
|
|
):
|
||
|
|
rendered = menu.render_component(self.request)
|
||
|
|
|
||
|
|
self.assertEqual(
|
||
|
|
rendered,
|
||
|
|
[
|
||
|
|
sidebar.LinkMenuItem("pages", "Pages", "/pages/"),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_submenu(self):
|
||
|
|
menu = Menu(register_hook_name="register_menu_item")
|
||
|
|
submenu = Menu(register_hook_name="register_submenu_item")
|
||
|
|
|
||
|
|
with hooks.register_temporarily(
|
||
|
|
[
|
||
|
|
(
|
||
|
|
"register_menu_item",
|
||
|
|
menu_item_hook("My lovely submenu", submenu, cls=SubmenuMenuItem),
|
||
|
|
),
|
||
|
|
("register_submenu_item", menu_item_hook("Pages", "/pages/")),
|
||
|
|
]
|
||
|
|
):
|
||
|
|
rendered = menu.render_component(self.request)
|
||
|
|
|
||
|
|
self.assertIsInstance(rendered, list)
|
||
|
|
self.assertEqual(len(rendered), 1)
|
||
|
|
self.assertIsInstance(rendered[0], sidebar.SubMenuItem)
|
||
|
|
self.assertEqual(rendered[0].name, "my-lovely-submenu")
|
||
|
|
self.assertEqual(rendered[0].label, "My lovely submenu")
|
||
|
|
self.assertListEqual(
|
||
|
|
rendered[0].menu_items,
|
||
|
|
[
|
||
|
|
sidebar.LinkMenuItem("pages", "Pages", "/pages/"),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_dismissible_initial(self):
|
||
|
|
menu = Menu(register_hook_name="register_menu_item")
|
||
|
|
submenu = Menu(register_hook_name="register_submenu_item")
|
||
|
|
|
||
|
|
with hooks.register_temporarily(
|
||
|
|
[
|
||
|
|
(
|
||
|
|
"register_menu_item",
|
||
|
|
menu_item_hook(
|
||
|
|
"My dismissible submenu",
|
||
|
|
submenu,
|
||
|
|
cls=DismissibleSubmenuMenuItem,
|
||
|
|
name="dismissible-submenu-menu-item",
|
||
|
|
),
|
||
|
|
),
|
||
|
|
(
|
||
|
|
"register_submenu_item",
|
||
|
|
menu_item_hook(
|
||
|
|
"Pages",
|
||
|
|
"/pages/",
|
||
|
|
cls=DismissibleMenuItem,
|
||
|
|
name="dismissible-menu-item",
|
||
|
|
),
|
||
|
|
),
|
||
|
|
]
|
||
|
|
):
|
||
|
|
rendered = menu.render_component(self.request)
|
||
|
|
|
||
|
|
self.assertIsInstance(rendered, list)
|
||
|
|
self.assertEqual(len(rendered), 1)
|
||
|
|
self.assertIsInstance(rendered[0], sidebar.SubMenuItem)
|
||
|
|
self.assertEqual(rendered[0].name, "dismissible-submenu-menu-item")
|
||
|
|
self.assertEqual(rendered[0].label, "My dismissible submenu")
|
||
|
|
self.assertEqual(
|
||
|
|
rendered[0].attrs,
|
||
|
|
# Should not be dismissed
|
||
|
|
{
|
||
|
|
"data-controller": "w-dismissible",
|
||
|
|
"data-w-dismissible-dismissed-class": "w-dismissible--dismissed",
|
||
|
|
"data-w-dismissible-id-value": "dismissible-submenu-menu-item",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
self.assertListEqual(
|
||
|
|
rendered[0].menu_items,
|
||
|
|
[
|
||
|
|
sidebar.LinkMenuItem(
|
||
|
|
"dismissible-menu-item",
|
||
|
|
"Pages",
|
||
|
|
"/pages/",
|
||
|
|
# Should not be dismissed
|
||
|
|
attrs={
|
||
|
|
"data-controller": "w-dismissible",
|
||
|
|
"data-w-dismissible-dismissed-class": "w-dismissible--dismissed",
|
||
|
|
"data-w-dismissible-id-value": "dismissible-menu-item",
|
||
|
|
},
|
||
|
|
),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_dismissible_dismissed(self):
|
||
|
|
self.profile.dismissibles = {
|
||
|
|
"dismissible-submenu-menu-item": True,
|
||
|
|
"dismissible-menu-item": True,
|
||
|
|
}
|
||
|
|
self.profile.save()
|
||
|
|
self.request.user.refresh_from_db()
|
||
|
|
|
||
|
|
menu = Menu(register_hook_name="register_menu_item")
|
||
|
|
submenu = Menu(register_hook_name="register_submenu_item")
|
||
|
|
|
||
|
|
with hooks.register_temporarily(
|
||
|
|
[
|
||
|
|
(
|
||
|
|
"register_menu_item",
|
||
|
|
menu_item_hook(
|
||
|
|
"My dismissible submenu",
|
||
|
|
submenu,
|
||
|
|
cls=DismissibleSubmenuMenuItem,
|
||
|
|
name="dismissible-submenu-menu-item",
|
||
|
|
),
|
||
|
|
),
|
||
|
|
(
|
||
|
|
"register_submenu_item",
|
||
|
|
menu_item_hook(
|
||
|
|
"Pages",
|
||
|
|
"/pages/",
|
||
|
|
cls=DismissibleMenuItem,
|
||
|
|
name="dismissible-menu-item",
|
||
|
|
),
|
||
|
|
),
|
||
|
|
]
|
||
|
|
):
|
||
|
|
rendered = menu.render_component(self.request)
|
||
|
|
|
||
|
|
self.assertIsInstance(rendered, list)
|
||
|
|
self.assertEqual(len(rendered), 1)
|
||
|
|
self.assertIsInstance(rendered[0], sidebar.SubMenuItem)
|
||
|
|
self.assertEqual(rendered[0].name, "dismissible-submenu-menu-item")
|
||
|
|
self.assertEqual(rendered[0].label, "My dismissible submenu")
|
||
|
|
self.assertEqual(
|
||
|
|
rendered[0].attrs,
|
||
|
|
{
|
||
|
|
"data-controller": "w-dismissible",
|
||
|
|
"data-w-dismissible-dismissed-class": "w-dismissible--dismissed",
|
||
|
|
"data-w-dismissible-id-value": "dismissible-submenu-menu-item",
|
||
|
|
# Should be dismissed
|
||
|
|
"data-w-dismissible-dismissed-value": "true",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
self.assertListEqual(
|
||
|
|
rendered[0].menu_items,
|
||
|
|
[
|
||
|
|
sidebar.LinkMenuItem(
|
||
|
|
"dismissible-menu-item",
|
||
|
|
"Pages",
|
||
|
|
"/pages/",
|
||
|
|
# Should be dismissed
|
||
|
|
attrs={
|
||
|
|
"data-controller": "w-dismissible",
|
||
|
|
"data-w-dismissible-dismissed-class": "w-dismissible--dismissed",
|
||
|
|
"data-w-dismissible-id-value": "dismissible-menu-item",
|
||
|
|
"data-w-dismissible-dismissed-value": "true",
|
||
|
|
},
|
||
|
|
),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_dismissible_no_userprofile(self):
|
||
|
|
# Without a user profile, dismissible menu items should not be dismissed
|
||
|
|
self.profile.delete()
|
||
|
|
self.request.user.refresh_from_db()
|
||
|
|
|
||
|
|
menu = Menu(register_hook_name="register_menu_item")
|
||
|
|
submenu = Menu(register_hook_name="register_submenu_item")
|
||
|
|
|
||
|
|
with hooks.register_temporarily(
|
||
|
|
[
|
||
|
|
(
|
||
|
|
"register_menu_item",
|
||
|
|
menu_item_hook(
|
||
|
|
"My dismissible submenu",
|
||
|
|
submenu,
|
||
|
|
cls=DismissibleSubmenuMenuItem,
|
||
|
|
name="dismissible-submenu-menu-item",
|
||
|
|
),
|
||
|
|
),
|
||
|
|
(
|
||
|
|
"register_submenu_item",
|
||
|
|
menu_item_hook(
|
||
|
|
"Pages",
|
||
|
|
"/pages/",
|
||
|
|
cls=DismissibleMenuItem,
|
||
|
|
name="dismissible-menu-item",
|
||
|
|
),
|
||
|
|
),
|
||
|
|
]
|
||
|
|
):
|
||
|
|
rendered = menu.render_component(self.request)
|
||
|
|
|
||
|
|
self.assertIsInstance(rendered, list)
|
||
|
|
self.assertEqual(len(rendered), 1)
|
||
|
|
self.assertIsInstance(rendered[0], sidebar.SubMenuItem)
|
||
|
|
self.assertEqual(rendered[0].name, "dismissible-submenu-menu-item")
|
||
|
|
self.assertEqual(rendered[0].label, "My dismissible submenu")
|
||
|
|
self.assertEqual(
|
||
|
|
rendered[0].attrs,
|
||
|
|
{
|
||
|
|
"data-controller": "w-dismissible",
|
||
|
|
"data-w-dismissible-dismissed-class": "w-dismissible--dismissed",
|
||
|
|
"data-w-dismissible-id-value": "dismissible-submenu-menu-item",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
self.assertListEqual(
|
||
|
|
rendered[0].menu_items,
|
||
|
|
[
|
||
|
|
sidebar.LinkMenuItem(
|
||
|
|
"dismissible-menu-item",
|
||
|
|
"Pages",
|
||
|
|
"/pages/",
|
||
|
|
attrs={
|
||
|
|
"data-controller": "w-dismissible",
|
||
|
|
"data-w-dismissible-dismissed-class": "w-dismissible--dismissed",
|
||
|
|
"data-w-dismissible-id-value": "dismissible-menu-item",
|
||
|
|
},
|
||
|
|
),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_admin_only_menuitem(self):
|
||
|
|
menu = Menu(register_hook_name="register_menu_item")
|
||
|
|
|
||
|
|
with hooks.register_temporarily(
|
||
|
|
[
|
||
|
|
("register_menu_item", menu_item_hook("Pages", "/pages/")),
|
||
|
|
(
|
||
|
|
"register_menu_item",
|
||
|
|
menu_item_hook(
|
||
|
|
"Secret pages", "/pages/secret/", cls=AdminOnlyMenuItem
|
||
|
|
),
|
||
|
|
),
|
||
|
|
]
|
||
|
|
):
|
||
|
|
rendered = menu.render_component(self.request)
|
||
|
|
self.request.user = self.create_user(username="non-admin")
|
||
|
|
rendered_non_admin = menu.render_component(self.request)
|
||
|
|
|
||
|
|
self.assertListEqual(
|
||
|
|
rendered,
|
||
|
|
[
|
||
|
|
sidebar.LinkMenuItem("pages", "Pages", "/pages/"),
|
||
|
|
sidebar.LinkMenuItem("secret-pages", "Secret pages", "/pages/secret/"),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
|
||
|
|
self.assertListEqual(
|
||
|
|
rendered_non_admin,
|
||
|
|
[
|
||
|
|
sidebar.LinkMenuItem("pages", "Pages", "/pages/"),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_menu_items_have_names(self):
|
||
|
|
# Delete the registered_menu_items cache
|
||
|
|
try:
|
||
|
|
del admin_menu.registered_menu_items
|
||
|
|
# The cache may not be created yet if the test is run in isolation
|
||
|
|
except AttributeError:
|
||
|
|
pass
|
||
|
|
|
||
|
|
# Generate the menu items using a different language
|
||
|
|
with translation.override("fr"):
|
||
|
|
names = {item.name for item in admin_menu.registered_menu_items}
|
||
|
|
|
||
|
|
# Default menu items
|
||
|
|
expected = {
|
||
|
|
"explorer",
|
||
|
|
"images",
|
||
|
|
"documents",
|
||
|
|
"snippets",
|
||
|
|
"forms",
|
||
|
|
"reports",
|
||
|
|
"settings",
|
||
|
|
"help",
|
||
|
|
}
|
||
|
|
|
||
|
|
# If some of the above items do not have a name, they will be
|
||
|
|
# automatically generated from the label, which is translatable.
|
||
|
|
# We want the name to be consistent across languages, so this test will
|
||
|
|
# fail if the label is translated.
|
||
|
|
self.assertFalse(expected - names)
|
||
|
|
|
||
|
|
def test_submenu_items_have_names(self):
|
||
|
|
names = set()
|
||
|
|
# Generate the submenu items using a different language
|
||
|
|
with translation.override("fr"):
|
||
|
|
# Getting only the submenu items
|
||
|
|
for item in admin_menu.registered_menu_items:
|
||
|
|
if not hasattr(item, "menu"):
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Delete the registered_menu_items cache
|
||
|
|
try:
|
||
|
|
del item.menu.registered_menu_items
|
||
|
|
# The cache may not be created yet if the test is run in isolation
|
||
|
|
except AttributeError:
|
||
|
|
pass
|
||
|
|
|
||
|
|
for subitem in item.menu.registered_menu_items:
|
||
|
|
names.add(subitem.name)
|
||
|
|
|
||
|
|
# Default submenu items
|
||
|
|
expected = {
|
||
|
|
"site-history",
|
||
|
|
"workflows",
|
||
|
|
"users",
|
||
|
|
"revisable-child-models",
|
||
|
|
"groups",
|
||
|
|
"aging-pages",
|
||
|
|
"editor-guide",
|
||
|
|
"sites",
|
||
|
|
"publishables",
|
||
|
|
"locked-pages",
|
||
|
|
"workflow-tasks",
|
||
|
|
"icon-site-setting",
|
||
|
|
"revisable-models",
|
||
|
|
"collections",
|
||
|
|
"locales",
|
||
|
|
"styleguide",
|
||
|
|
"test-generic-setting",
|
||
|
|
"test-site-setting",
|
||
|
|
"important-pages-generic-setting",
|
||
|
|
"redirects",
|
||
|
|
"important-pages-site-setting",
|
||
|
|
"workflow-tasks",
|
||
|
|
"promoted-search-results",
|
||
|
|
"icon-generic-setting",
|
||
|
|
"file-site-setting",
|
||
|
|
"workflows",
|
||
|
|
"file-generic-setting",
|
||
|
|
}
|
||
|
|
|
||
|
|
# If some of the above subitems do not have a name, they will be
|
||
|
|
# automatically generated from the label, which is translatable.
|
||
|
|
# We want the name to be consistent across languages, so this test will
|
||
|
|
# fail if the label is translated.
|
||
|
|
self.assertFalse(expected - names)
|