diff --git a/docs/reference/hooks.md b/docs/reference/hooks.md index 4f37b67ce7..819178082e 100644 --- a/docs/reference/hooks.md +++ b/docs/reference/hooks.md @@ -407,18 +407,29 @@ Return a QuerySet of `Permission` objects to be shown in the Groups administrati ### `register_user_listing_buttons` -Add buttons to the user list. This example will add a simple button to the listing: +Add buttons to the user list. + +This hook takes two parameters: +- `user`: The user object to generate the button for +- `request_user`: The currently logged-in user + +This example will add a simple button to the listing if the currently logged-in user is a superuser: ```python from wagtail.users.widgets import UserListingButton @hooks.register("register_user_listing_buttons") -def user_listing_external_profile(context, user): - yield UserListingButton( - "Show profile", - f"/goes/to/a/url/{user.pk}", - priority=30, - ) +def user_listing_external_profile(user, request_user): + if request_user.is_superuser: + yield UserListingButton( + "Show profile", + f"/goes/to/a/url/{user.pk}", + priority=30, + ) +``` + +```{versionchanged} 6.2 +The hook function was updated to accept a `request_user` argument instead of `context`. ``` (filter_form_submissions_for_user)= diff --git a/docs/releases/6.1.md b/docs/releases/6.1.md index f817b78bf2..5e192b3bb9 100644 --- a/docs/releases/6.1.md +++ b/docs/releases/6.1.md @@ -108,8 +108,17 @@ As part of the Universal Listings project, the `SubmissionsListView` for listing - The `ordering` attribute has been renamed to `default_ordering`. - The template used to render the view has been significantly refactored to use the new universal listings UI. + +### `register_user_listing_buttons` hook signature changed + +The function signature for the [`register_user_listing_buttons`](register_user_listing_buttons) hook was updated to accept a `request_user` argument instead of `context`. If you use this hook, make sure to update your function signature to match the new one. The old signature with `context` will continue to work for now, but the context only contains the request object. Support for the old signature will be removed in a future release. + ## Upgrade considerations - changes to undocumented internals +### Deprecation of `user_listing_buttons` template tag + +The undocumented `user_listing_buttons` template tag has been deprecated and will be removed in a future release. + ### Deprecation of `window.chooserUrls` within Draftail choosers The undocumented usage of the JavaScript `window.chooserUrls` within Draftail choosers will be removed in a future release. diff --git a/wagtail/users/templatetags/wagtailusers_tags.py b/wagtail/users/templatetags/wagtailusers_tags.py index 37603b37c6..bc4f6b922f 100644 --- a/wagtail/users/templatetags/wagtailusers_tags.py +++ b/wagtail/users/templatetags/wagtailusers_tags.py @@ -1,5 +1,5 @@ -import itertools from collections import defaultdict +from warnings import warn from django import template from django.contrib.auth import get_permission_codename @@ -9,7 +9,9 @@ from django.utils.text import camel_case_to_spaces from wagtail import hooks from wagtail.admin.models import Admin +from wagtail.coreutils import accepts_kwarg from wagtail.users.permission_order import CONTENT_TYPE_ORDER +from wagtail.utils.deprecation import RemovedInWagtail70Warning register = template.Library() @@ -185,8 +187,24 @@ def format_permissions(permission_bound_field): @register.inclusion_tag("wagtailadmin/shared/buttons.html", takes_context=True) def user_listing_buttons(context, user): - button_hooks = hooks.get_hooks("register_user_listing_buttons") - buttons = sorted( - itertools.chain.from_iterable(hook(context, user) for hook in button_hooks) + warn( + "`user_listing_buttons` template tag is deprecated.", + category=RemovedInWagtail70Warning, ) - return {"user": user, "buttons": buttons} + + buttons = [] + + for hook in hooks.get_hooks("register_user_listing_buttons"): + if accepts_kwarg(hook, "request_user"): + buttons.extend(hook(user=user, request_user=context.get("request").user)) + else: + # old-style hook that accepts a context argument instead of request_user + buttons.extend(hook(context, user)) + warn( + "`register_user_listing_buttons` hook functions should accept a " + "`request_user` argument instead of `context` - " + f"{hook.__module__}.{hook.__name__} needs to be updated", + category=RemovedInWagtail70Warning, + ) + + return {"user": user, "buttons": sorted(buttons)} diff --git a/wagtail/users/tests/test_admin_views.py b/wagtail/users/tests/test_admin_views.py index 20654eae7e..0087ba4b4d 100644 --- a/wagtail/users/tests/test_admin_views.py +++ b/wagtail/users/tests/test_admin_views.py @@ -10,6 +10,7 @@ from django.core.exceptions import ImproperlyConfigured from django.core.files.uploadedfile import SimpleUploadedFile from django.db.models import Q from django.http import HttpRequest, HttpResponse +from django.template import RequestContext, Template from django.test import TestCase, override_settings from django.urls import reverse from django.utils.text import capfirst @@ -18,7 +19,9 @@ from wagtail import hooks from wagtail.admin.admin_url_finder import AdminURLFinder from wagtail.admin.models import Admin from wagtail.admin.staticfiles import versioned_static +from wagtail.admin.widgets.button import ButtonWithDropdown from wagtail.compat import AUTH_USER_APP_LABEL, AUTH_USER_MODEL_NAME +from wagtail.coreutils import get_dummy_request from wagtail.log_actions import log from wagtail.models import ( Collection, @@ -34,6 +37,7 @@ from wagtail.users.permission_order import register as register_permission_order from wagtail.users.views.groups import GroupViewSet from wagtail.users.views.users import get_user_creation_form, get_user_edit_form from wagtail.users.wagtail_hooks import get_group_viewset_cls +from wagtail.users.widgets import UserListingButton from wagtail.utils.deprecation import RemovedInWagtail70Warning delete_user_perm_codename = f"delete_{AUTH_USER_MODEL_NAME.lower()}" @@ -197,7 +201,7 @@ class TestUserIndexView(AdminTemplateTestUtils, WagtailTestUtils, TestCase): first_name="First Name", last_name="Last Name", ) - self.login() + self.user = self.login() def get(self, params={}): return self.client.get(reverse("wagtailusers_users:index"), params) @@ -273,6 +277,60 @@ class TestUserIndexView(AdminTemplateTestUtils, WagtailTestUtils, TestCase): with self.assertNumQueries(num_queries): self.get() + def test_buttons_hook(self): + def hook(user, request_user): + self.assertEqual(request_user, self.user) + yield UserListingButton( + "Show profile", + f"/goes/to/a/url/{user.pk}", + priority=30, + ) + yield ButtonWithDropdown( + label="Moar pls!", + buttons=[UserListingButton("Alrighty", "/cheers", priority=10)], + ) + + with self.register_hook("register_user_listing_buttons", hook): + response = self.get() + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "wagtailadmin/shared/buttons.html") + + soup = self.get_soup(response.content) + row = soup.select_one(f"tbody tr:has([data-object-id='{self.test_user.pk}'])") + self.assertIsNotNone(row) + + profile_url = f"/goes/to/a/url/{self.test_user.pk}" + actions = row.select_one("td ul.actions") + top_level_custom_button = actions.select_one(f"li > a[href='{profile_url}']") + self.assertIsNone(top_level_custom_button) + custom_button = actions.select_one( + f"li [data-controller='w-dropdown'] a[href='{profile_url}']" + ) + self.assertIsNotNone(custom_button) + self.assertEqual( + custom_button.text.strip(), + "Show profile", + ) + + nested_dropdown = actions.select_one( + "li [data-controller='w-dropdown'] [data-controller='w-dropdown']" + ) + self.assertIsNone(nested_dropdown) + dropdown_buttons = actions.select("li > [data-controller='w-dropdown']") + # Default "More" button and the custom "Moar pls!" button + self.assertEqual(len(dropdown_buttons), 2) + custom_dropdown = None + for button in dropdown_buttons: + if "Moar pls!" in button.text.strip(): + custom_dropdown = button + self.assertIsNotNone(custom_dropdown) + self.assertEqual(custom_dropdown.select_one("button").text.strip(), "Moar pls!") + # Should contain the custom button inside the custom dropdown + custom_button = custom_dropdown.find("a", attrs={"href": "/cheers"}) + self.assertIsNotNone(custom_button) + self.assertEqual(custom_button.text.strip(), "Alrighty") + class TestUserIndexResultsView(AdminTemplateTestUtils, WagtailTestUtils, TestCase): def setUp(self): @@ -2637,3 +2695,100 @@ class TestAuthorisationDeleteView(WagtailTestUtils, TestCase): self.assertRedirects(response, reverse("wagtailusers_users:index")) user = get_user_model().objects.filter(email="test_user@email.com") self.assertFalse(user.exists()) + + +class TestTemplateTags(WagtailTestUtils, TestCase): + @classmethod + def setUpTestData(cls): + cls.user = cls.create_superuser("admin") + cls.request = get_dummy_request() + cls.request.user = cls.user + cls.test_user = cls.create_user( + username="testuser", + email="testuser@email.com", + password="password", + ) + + def test_user_listing_buttons(self): + template = """ + {% load wagtailusers_tags %} + {% for user in users %} +