Repository: wagtail/wagtail

Issue:
Remove unused userbar views
The userbar was built within the iframe, with Django views that render the iframe's content.

- It first appeared in edd757c5787fd971f620902f6421cc1b51a2b76b.
- Then further refactored in 637899f6c2b82a27bc774121be2dac5c97f6f94c and 5e17881f2d85e8f153c88fdf3bb35e4eecdfcc07. Take note of the latter in particular, where the view was split into `for_frontend` and `for_moderation` views.
- There was a discussion in #2164 which suggested refactoring of the userbar to not use an iframe.
- #2201 did away with the refactoring, removed the iframe and the `wagtailadmin/userbar/frame.html` template.
- Unfortunately, the refactoring did not remove the Django views for the iframe's content, leaving the code unused for 7+ years.

I believe the views are not used anywhere by us and they're very unlikely to be used by developers. As far as developers are concerned, they've been using the `{% wagtailuserbar %}` tag which abstracts the implementation details, including whether it used the iframe or not.

I suggest we remove:
- The whole file for the views https://github.com/wagtail/wagtail/blob/main/wagtail/admin/views/userbar.py
- The URLs https://github.com/wagtail/wagtail/blob/a1bd357092bc7107c3d41477536f471a1f3f307d/wagtail/admin/urls/__init__.py#L135-L146
- The tests https://github.com/wagtail/wagtail/blob/a1bd357092bc7107c3d41477536f471a1f3f307d/wagtail/admin/tests/test_userbar.py#L362-L483
Remove unused userbar views
The userbar was built within the iframe, with Django views that render the iframe's content.

- It first appeared in edd757c5787fd971f620902f6421cc1b51a2b76b.
- Then further refactored in 637899f6c2b82a27bc774121be2dac5c97f6f94c and 5e17881f2d85e8f153c88fdf3bb35e4eecdfcc07. Take note of the latter in particular, where the view was split into `for_frontend` and `for_moderation` views.
- There was a discussion in #2164 which suggested refactoring of the userbar to not use an iframe.
- #2201 did away with the refactoring, removed the iframe and the `wagtailadmin/userbar/frame.html` template.
- Unfortunately, the refactoring did not remove the Django views for the iframe's content, leaving the code unused for 7+ years.

I believe the views are not used anywhere by us and they're very unlikely to be used by developers. As far as developers are concerned, they've been using the `{% wagtailuserbar %}` tag which abstracts the implementation details, including whether it used the iframe or not.

I suggest we remove:
- The whole file for the views https://github.com/wagtail/wagtail/blob/main/wagtail/admin/views/userbar.py
- The URLs https://github.com/wagtail/wagtail/blob/a1bd357092bc7107c3d41477536f471a1f3f307d/wagtail/admin/urls/__init__.py#L135-L146
- The tests https://github.com/wagtail/wagtail/blob/a1bd357092bc7107c3d41477536f471a1f3f307d/wagtail/admin/tests/test_userbar.py#L362-L483


Discussion:

Resolving patch:
diff --git a/wagtail/admin/urls/__init__.py b/wagtail/admin/urls/__init__.py
--- a/wagtail/admin/urls/__init__.py
+++ b/wagtail/admin/urls/__init__.py
@@ -17,7 +17,7 @@
 from wagtail.admin.urls import password_reset as wagtailadmin_password_reset_urls
 from wagtail.admin.urls import reports as wagtailadmin_reports_urls
 from wagtail.admin.urls import workflows as wagtailadmin_workflows_urls
-from wagtail.admin.views import account, chooser, dismissibles, home, tags, userbar
+from wagtail.admin.views import account, chooser, dismissibles, home, tags
 from wagtail.admin.views.bulk_action import index as bulk_actions
 from wagtail.admin.views.pages import listing
 from wagtail.utils.urlpatterns import decorate_urlpatterns
@@ -132,18 +132,6 @@ def get_sprite_hash():
 urlpatterns += [
     path(f"sprite-{get_sprite_hash()}/", home.sprite, name="wagtailadmin_sprite"),
     path("login/", account.LoginView.as_view(), name="wagtailadmin_login"),
-    # These two URLs have the "permission_required" decorator applied directly
-    # as they need to fail with a 403 error rather than redirect to the login page
-    path(
-        "userbar/<int:page_id>/",
-        userbar.for_frontend,
-        name="wagtailadmin_userbar_frontend",
-    ),
-    path(
-        "userbar/moderation/<int:revision_id>/",
-        userbar.for_moderation,
-        name="wagtailadmin_userbar_moderation",
-    ),
     # Password reset
     path("password_reset/", include(wagtailadmin_password_reset_urls)),
     # JS translation catalog
diff --git a/wagtail/admin/views/userbar.py b/wagtail/admin/views/userbar.py
deleted file mode 100644
--- a/wagtail/admin/views/userbar.py
+++ /dev/null
@@ -1,65 +0,0 @@
-from django.contrib.auth.decorators import permission_required
-from django.template.response import TemplateResponse
-
-from wagtail import hooks
-from wagtail.admin.userbar import (
-    AddPageItem,
-    ApproveModerationEditPageItem,
-    EditPageItem,
-    RejectModerationEditPageItem,
-)
-from wagtail.models import Page, Revision
-
-
-@permission_required("wagtailadmin.access_admin", raise_exception=True)
-def for_frontend(request, page_id):
-    items = [
-        EditPageItem(Page.objects.get(id=page_id)),
-        AddPageItem(Page.objects.get(id=page_id)),
-    ]
-
-    for fn in hooks.get_hooks("construct_wagtail_userbar"):
-        fn(request, items)
-
-    # Render the items
-    rendered_items = [item.render(request) for item in items]
-
-    # Remove any unrendered items
-    rendered_items = [item for item in rendered_items if item]
-
-    # Render the edit bird
-    return TemplateResponse(
-        request,
-        "wagtailadmin/userbar/base.html",
-        {
-            "items": rendered_items,
-        },
-    )
-
-
-@permission_required("wagtailadmin.access_admin", raise_exception=True)
-def for_moderation(request, revision_id):
-    items = [
-        EditPageItem(Revision.page_revisions.get(id=revision_id).content_object),
-        AddPageItem(Revision.page_revisions.get(id=revision_id).content_object),
-        ApproveModerationEditPageItem(Revision.page_revisions.get(id=revision_id)),
-        RejectModerationEditPageItem(Revision.page_revisions.get(id=revision_id)),
-    ]
-
-    for fn in hooks.get_hooks("construct_wagtail_userbar"):
-        fn(request, items)
-
-    # Render the items
-    rendered_items = [item.render(request) for item in items]
-
-    # Remove any unrendered items
-    rendered_items = [item for item in rendered_items if item]
-
-    # Render the edit bird
-    return TemplateResponse(
-        request,
-        "wagtailadmin/userbar/base.html",
-        {
-            "items": rendered_items,
-        },
-    )


Test patch:
diff --git a/wagtail/admin/tests/test_userbar.py b/wagtail/admin/tests/test_userbar.py
--- a/wagtail/admin/tests/test_userbar.py
+++ b/wagtail/admin/tests/test_userbar.py
@@ -8,8 +8,8 @@
 from wagtail import hooks
 from wagtail.admin.userbar import AccessibilityItem
 from wagtail.coreutils import get_dummy_request
-from wagtail.models import PAGE_TEMPLATE_VAR, Page
-from wagtail.test.testapp.models import BusinessChild, BusinessIndex
+from wagtail.models import PAGE_TEMPLATE_VAR, Page, Site
+from wagtail.test.testapp.models import BusinessChild, BusinessIndex, SimplePage
 from wagtail.test.utils import WagtailTestUtils
 
 
@@ -359,38 +359,42 @@ def get_axe_rules(self, request):
             )
 
 
-class TestUserbarFrontend(WagtailTestUtils, TestCase):
+class TestUserbarInPageServe(WagtailTestUtils, TestCase):
     def setUp(self):
-        self.login()
-        self.homepage = Page.objects.get(id=2)
+        self.user = self.login()
+        self.request = get_dummy_request(site=Site.objects.first())
+        self.request.user = self.user
+        self.homepage = Page.objects.get(id=2).specific
+        # Use a specific page model to use our template that has {% wagtailuserbar %}
+        self.page = SimplePage(title="Rendang", content="Enak", live=True)
+        self.homepage.add_child(instance=self.page)
 
-    def test_userbar_frontend(self):
-        response = self.client.get(
-            reverse("wagtailadmin_userbar_frontend", args=(self.homepage.id,))
-        )
+    def test_userbar_rendered(self):
+        response = self.page.serve(self.request)
+        response.render()
 
         self.assertEqual(response.status_code, 200)
-        self.assertTemplateUsed(response, "wagtailadmin/userbar/base.html")
-
-    def test_userbar_frontend_anonymous_user_cannot_see(self):
-        # Logout
-        self.client.logout()
+        self.assertContains(response, '<template id="wagtail-userbar-template">')
 
-        response = self.client.get(
-            reverse("wagtailadmin_userbar_frontend", args=(self.homepage.id,))
-        )
+    def test_userbar_anonymous_user_cannot_see(self):
+        self.request.user = AnonymousUser()
+        response = self.page.serve(self.request)
+        response.render()
 
-        # Check that the user received a forbidden message
-        self.assertEqual(response.status_code, 403)
+        # Check that the userbar is not rendered
+        self.assertEqual(response.status_code, 200)
+        self.assertNotContains(response, '<template id="wagtail-userbar-template">')
 
 
 class TestUserbarAddLink(WagtailTestUtils, TestCase):
     fixtures = ["test.json"]
 
     def setUp(self):
-        self.login()
+        self.user = self.login()
+        self.request = get_dummy_request(site=Site.objects.first())
+        self.request.user = self.user
         self.homepage = Page.objects.get(url_path="/home/")
-        self.event_index = Page.objects.get(url_path="/home/events/")
+        self.event_index = Page.objects.get(url_path="/home/events/").specific
 
         self.business_index = BusinessIndex(title="Business", live=True)
         self.homepage.add_child(instance=self.business_index)
@@ -399,9 +403,9 @@ def setUp(self):
         self.business_index.add_child(instance=self.business_child)
 
     def test_page_allowing_subpages(self):
-        response = self.client.get(
-            reverse("wagtailadmin_userbar_frontend", args=(self.event_index.id,))
-        )
+        response = self.event_index.serve(self.request)
+        response.render()
+        self.assertEqual(response.status_code, 200)
 
         # page allows subpages, so the 'add page' button should show
         expected_url = reverse(
@@ -415,37 +419,41 @@ def test_page_allowing_subpages(self):
                 Add a child page
             </a>
             """
-        self.assertTagInHTML(needle, str(response.content))
+        self.assertTagInHTML(needle, response.content.decode())
 
     def test_page_disallowing_subpages(self):
-        response = self.client.get(
-            reverse("wagtailadmin_userbar_frontend", args=(self.business_child.id,))
-        )
+        response = self.business_child.serve(self.request)
+        response.render()
+        self.assertEqual(response.status_code, 200)
 
         # page disallows subpages, so the 'add page' button shouldn't show
         expected_url = reverse(
             "wagtailadmin_pages:add_subpage", args=(self.business_index.id,)
         )
-        expected_link = (
-            '<a href="%s" target="_parent">Add a child page</a>' % expected_url
-        )
-        self.assertNotContains(response, expected_link)
+        soup = self.get_soup(response.content)
+        link = soup.find("a", attrs={"href": expected_url})
+        self.assertIsNone(link)
 
 
 class TestUserbarModeration(WagtailTestUtils, TestCase):
     def setUp(self):
-        self.login()
+        self.user = self.login()
+        self.request = get_dummy_request(site=Site.objects.first())
+        self.request.user = self.user
         self.homepage = Page.objects.get(id=2)
-        self.homepage.save_revision(submitted_for_moderation=True)
-        self.revision = self.homepage.get_latest_revision()
+        # Use a specific page model to use our template that has {% wagtailuserbar %}
+        self.page = SimplePage(title="Martabak", content="Lezat", live=True)
+        self.homepage.add_child(instance=self.page)
+        self.page.save_revision(submitted_for_moderation=True)
+        self.revision = self.page.get_latest_revision()
+        self.request.revision_id = self.revision.id
 
     def test_userbar_moderation(self):
-        response = self.client.get(
-            reverse("wagtailadmin_userbar_moderation", args=(self.revision.id,))
-        )
+        response = self.page.serve(self.request)
+        response.render()
 
         self.assertEqual(response.status_code, 200)
-        self.assertTemplateUsed(response, "wagtailadmin/userbar/base.html")
+        self.assertContains(response, '<template id="wagtail-userbar-template">')
 
         expected_approve_html = """
             <form action="/admin/pages/moderation/{}/approve/" target="_parent" method="post">
@@ -457,7 +465,7 @@ def test_userbar_moderation(self):
         """.format(
             self.revision.id
         )
-        self.assertTagInHTML(expected_approve_html, str(response.content))
+        self.assertTagInHTML(expected_approve_html, response.content.decode())
 
         expected_reject_html = """
             <form action="/admin/pages/moderation/{}/reject/" target="_parent" method="post">
@@ -469,15 +477,14 @@ def test_userbar_moderation(self):
         """.format(
             self.revision.id
         )
-        self.assertTagInHTML(expected_reject_html, str(response.content))
+        self.assertTagInHTML(expected_reject_html, response.content.decode())
 
     def test_userbar_moderation_anonymous_user_cannot_see(self):
-        # Logout
-        self.client.logout()
+        self.request.user = AnonymousUser()
 
-        response = self.client.get(
-            reverse("wagtailadmin_userbar_moderation", args=(self.revision.id,))
-        )
+        response = self.page.serve(self.request)
+        response.render()
 
         # Check that the user received a forbidden message
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 200)
+        self.assertNotContains(response, '<template id="wagtail-userbar-template">')