浏览代码

Add ability to check permission on parent `PanelGroup` class

- Used by TabbedInterface, ObjectList, FieldRowPanel, MultiFieldPanel
Oliver Parker 2 年之前
父节点
当前提交
19fd2ceb98

+ 1 - 0
CHANGELOG.txt

@@ -54,6 +54,7 @@ Changelog
  * The `image_url` template tag, when using the serve view to redirect rather than serve directly, will now use temporary redirects with a cache header instead of permanent redirects (Jake Howard)
  * Add new test assertions to `WagtailPageTestCase` - `assertPageIsRoutable`, `assertPageIsRenderable`, `assertPageIsEditable`, `assertPageIsPreviewable` (Andy Babic)
  * Add documentation to the performance section about how to better create image URLs when not used directly on the page (Jake Howard)
+ * Add ability to provide a required `permission` to `PanelGroup`, used by `TabbedInterface`, `ObjectList`, `FieldRowPanel` and `MultiFieldPanel` (Oliver Parker)
  * Fix: Prevent `PageQuerySet.not_public` from returning all pages when no page restrictions exist (Mehrdad Moradizadeh)
  * Fix: Ensure that duplicate block ids are unique when duplicating stream blocks in the page editor (Joshua Munn)
  * Fix: Revise colour usage so that privacy & locked indicators can be seen in Windows High Contrast mode (LB (Ben Johnston))

+ 1 - 0
docs/extending/forms.md

@@ -65,6 +65,7 @@ See [](/reference/pages/panels) for the set of panel types provided by Wagtail.
 A view performs the following steps to render a model form through the panels mechanism:
 
 -   The top-level panel object for the model is retrieved. Usually this is done by looking up the model's `edit_handler` property and falling back on an `ObjectList` consisting of children given by the model's `panels` property. However, it may come from elsewhere - for example, the ModelAdmin module allows defining it on the ModelAdmin configuration object.
+-   If the `PanelsGroup`s permissions do not allow a user to see this panel, then nothing more will be done.
 -   The view calls `bind_to_model` on the top-level panel, passing the model class, and this returns a clone of the panel with a `model` property. As part of this process the `on_model_bound` method is invoked on each child panel, to allow it to perform additional initialisation that requires access to the model (for example, this is where `FieldPanel` retrieves the model field definition).
 -   The view then calls `get_form_class` on the top-level panel to retrieve a ModelForm subclass that can be used to edit the model. This proceeds as follows:
     -   Retrieve a base form class from the model's `base_form_class` property, falling back on `wagtail.admin.forms.WagtailAdminModelForm`

+ 15 - 5
docs/reference/pages/panels.md

@@ -77,13 +77,18 @@ Here are some Wagtail-specific types that you might include as fields in your mo
 
         A ``list`` or ``tuple`` of child panels
 
-    .. attribute:: MultiFieldPanel.heading
+    .. attribute:: MultiFieldPanel.heading (optional)
 
         A heading for the fields
 
-    .. attribute:: MultiFieldPanel.help_text
+    .. attribute:: MultiFieldPanel.help_text (optional)
 
         Help text to be displayed against the panel.
+
+    .. attribute:: MultiFieldPanel.permission (optional)
+        Allows a panel to be selectively shown to users with sufficient permission. Accepts a permission codename such as ``'myapp.change_blog_category'`` - if the logged-in user does not have that permission, the field will be omitted from the form.
+        Similar to `FieldPanel.permission`
+        The panel group will not be visible if the permission check does not pass.
 ```
 
 ### InlinePanel
@@ -103,7 +108,7 @@ Note that you can use `classname="collapsed"` to load the panel collapsed under
 ### FieldRowPanel
 
 ```{eval-rst}
-.. class:: FieldRowPanel(children, classname=None)
+.. class:: FieldRowPanel(children, classname=None, permission=None)
 
     This panel creates a columnar layout in the editing interface, where each of the child Panels appears alongside each other rather than below.
 
@@ -113,13 +118,18 @@ Note that you can use `classname="collapsed"` to load the panel collapsed under
 
         A ``list`` or ``tuple`` of child panels to display on the row
 
-    .. attribute:: FieldRowPanel.classname
+    .. attribute:: FieldRowPanel.classname (optional)
 
         A class to apply to the FieldRowPanel as a whole
 
-    .. attribute:: FieldRowPanel.help_text
+    .. attribute:: FieldRowPanel.help_text (optional)
 
         Help text to be displayed against the panel.
+
+    .. attribute:: FieldRowPanel.permission (optional)
+        Allows a panel to be selectively shown to users with sufficient permission. Accepts a permission codename such as ``'myapp.change_blog_category'`` - if the logged-in user does not have that permission, the field will be omitted from the form.
+        Similar to `FieldPanel.permission`
+        The panel group will not be visible if the permission check does not pass.
 ```
 
 ### HelpPanel

+ 1 - 0
docs/releases/4.1.md

@@ -97,6 +97,7 @@ There are multiple improvements to the documentation theme this release, here ar
  * The `image_url` template tag, when using the serve view to redirect rather than serve directly, will now use temporary redirects with a cache header instead of permanent redirects (Jake Howard)
  * Add new test assertions to `WagtailPageTestCase` - `assertPageIsRoutable`, `assertPageIsRenderable`, `assertPageIsEditable`, `assertPageIsPreviewable` (Andy Babic)
  * Add documentation to the performance section about how to better create image URLs when not used directly on the page (Jake Howard)
+ * Add ability to provide a required `permission` to `PanelGroup`, used by `TabbedInterface`, `ObjectList`, `FieldRowPanel` and `MultiFieldPanel` (Oliver Parker)
 
 ### Bug fixes
 

+ 12 - 0
wagtail/admin/panels.py

@@ -430,12 +430,15 @@ class PanelGroup(Panel):
     """
 
     def __init__(self, children=(), *args, **kwargs):
+        permission = kwargs.pop("permission", None)
         super().__init__(*args, **kwargs)
         self.children = children
+        self.permission = permission
 
     def clone_kwargs(self):
         kwargs = super().clone_kwargs()
         kwargs["children"] = self.children
+        kwargs["permission"] = self.permission
         return kwargs
 
     def get_form_options(self):
@@ -543,6 +546,15 @@ class PanelGroup(Panel):
             return any(child.show_panel_furniture() for child in self.children)
 
         def is_shown(self):
+            """
+            Check permissions on the panel group overall then check if any children
+            are shown.
+            """
+
+            if self.panel.permission:
+                if not self.request.user.has_perm(self.panel.permission):
+                    return False
+
             return any(child.is_shown() for child in self.children)
 
         @property

+ 112 - 41
wagtail/admin/tests/test_edit_handlers.py

@@ -5,7 +5,7 @@ from unittest import mock
 from django import forms
 from django.conf import settings
 from django.contrib.auth import get_user_model
-from django.contrib.auth.models import AnonymousUser
+from django.contrib.auth.models import AnonymousUser, Permission
 from django.core import checks
 from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
 from django.test import RequestFactory, TestCase, override_settings
@@ -374,6 +374,9 @@ class TestTabbedInterface(TestCase, WagtailTestUtils):
         user = self.create_superuser(username="admin")
         self.request.user = user
         self.user = self.login()
+        self.other_user = self.create_user(username="admin2", email="test2@email.com")
+        p = Permission.objects.get(codename="custom_see_panel_setting")
+        self.other_user.user_permissions.add(p)
         # a custom tabbed interface for EventPage
         self.event_page_tabbed_interface = TabbedInterface(
             [
@@ -398,6 +401,20 @@ class TestTabbedInterface(TestCase, WagtailTestUtils):
                     ],
                     heading="Secret",
                 ),
+                ObjectList(
+                    [
+                        FieldPanel("cost"),
+                    ],
+                    permission="tests.custom_see_panel_setting",
+                    heading="Custom Setting",
+                ),
+                ObjectList(
+                    [
+                        FieldPanel("cost"),
+                    ],
+                    permission="tests.other_custom_see_panel_setting",
+                    heading="Other Custom Setting",
+                ),
             ]
         ).bind_to_model(EventPage)
 
@@ -477,47 +494,101 @@ class TestTabbedInterface(TestCase, WagtailTestUtils):
         event = EventPage(title="Abergavenny sheepdog trials")
         form = EventPageForm(instance=event)
 
-        # when signed in as a superuser all three tabs should be visible
-        tabbed_interface = self.event_page_tabbed_interface.get_bound_panel(
-            instance=event,
-            form=form,
-            request=self.request,
-        )
-        result = tabbed_interface.render_html()
-        self.assertIn(
-            '<a id="tab-label-event_details" href="#tab-event_details" class="w-tabs__tab shiny" role="tab" aria-selected="false" tabindex="-1">',
-            result,
-        )
-        self.assertIn(
-            '<a id="tab-label-speakers" href="#tab-speakers" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
-            result,
-        )
-        self.assertIn(
-            '<a id="tab-label-secret" href="#tab-secret" ',
-            result,
-        )
+        with self.subTest("Super user test"):
+            # when signed in as a superuser all tabs should be visible
+            tabbed_interface = self.event_page_tabbed_interface.get_bound_panel(
+                instance=event,
+                form=form,
+                request=self.request,
+            )
+            result = tabbed_interface.render_html()
+            self.assertIn(
+                '<a id="tab-label-event_details" href="#tab-event_details" class="w-tabs__tab shiny" role="tab" aria-selected="false" tabindex="-1">',
+                result,
+            )
+            self.assertIn(
+                '<a id="tab-label-speakers" href="#tab-speakers" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
+                result,
+            )
+            self.assertIn(
+                '<a id="tab-label-secret" href="#tab-secret" ',
+                result,
+            )
+            self.assertIn(
+                '<a id="tab-label-custom_setting" href="#tab-custom_setting" ',
+                result,
+            )
+            self.assertIn(
+                '<a id="tab-label-other_custom_setting" href="#tab-other_custom_setting" ',
+                result,
+            )
 
-        # Login as non superuser to check that the third tab does not show
-        user = AnonymousUser()  # technically, Anonymous users cannot access the admin
-        self.request.user = user
-        tabbed_interface = self.event_page_tabbed_interface.get_bound_panel(
-            instance=event,
-            form=form,
-            request=self.request,
-        )
-        result = tabbed_interface.render_html()
-        self.assertIn(
-            '<a id="tab-label-event_details" href="#tab-event_details" class="w-tabs__tab shiny" role="tab" aria-selected="false" tabindex="-1">',
-            result,
-        )
-        self.assertIn(
-            '<a id="tab-label-speakers" href="#tab-speakers" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
-            result,
-        )
-        self.assertNotIn(
-            '<a id="tab-label-secret" href="#tab-secret" ',
-            result,
-        )
+        with self.subTest("Not superuser permissions"):
+            """
+            The super user panel should not show, nor should the panel they dont have
+            permission for.
+            """
+            self.request.user = self.other_user
+
+            tabbed_interface = self.event_page_tabbed_interface.get_bound_panel(
+                instance=event,
+                form=form,
+                request=self.request,
+            )
+            result = tabbed_interface.render_html()
+            self.assertIn(
+                '<a id="tab-label-event_details" href="#tab-event_details" class="w-tabs__tab shiny" role="tab" aria-selected="false" tabindex="-1">',
+                result,
+            )
+            self.assertIn(
+                '<a id="tab-label-speakers" href="#tab-speakers" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
+                result,
+            )
+            self.assertNotIn(
+                '<a id="tab-label-secret" href="#tab-secret" ',
+                result,
+            )
+            self.assertIn(
+                '<a id="tab-label-custom_setting" href="#tab-custom_setting" ',
+                result,
+            )
+            self.assertNotIn(
+                '<a id="tab-label-other_custom_setting" href="#tab-other-custom_setting" ',
+                result,
+            )
+
+        with self.subTest("Non superuser"):
+            # Login as non superuser to check that the third tab does not show
+            user = (
+                AnonymousUser()
+            )  # technically, Anonymous users cannot access the admin
+            self.request.user = user
+            tabbed_interface = self.event_page_tabbed_interface.get_bound_panel(
+                instance=event,
+                form=form,
+                request=self.request,
+            )
+            result = tabbed_interface.render_html()
+            self.assertIn(
+                '<a id="tab-label-event_details" href="#tab-event_details" class="w-tabs__tab shiny" role="tab" aria-selected="false" tabindex="-1">',
+                result,
+            )
+            self.assertIn(
+                '<a id="tab-label-speakers" href="#tab-speakers" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
+                result,
+            )
+            self.assertNotIn(
+                '<a id="tab-label-secret" href="#tab-secret" ',
+                result,
+            )
+            self.assertNotIn(
+                '<a id="tab-label-custom_setting" href="#tab-custom_setting" ',
+                result,
+            )
+            self.assertNotIn(
+                '<a id="tab-label-other_custom_setting" href="#tab-other-custom_setting" ',
+                result,
+            )
 
 
 class TestObjectList(TestCase):

+ 22 - 0
wagtail/test/testapp/migrations/0009_alter_eventpage_options.py

@@ -0,0 +1,22 @@
+# Generated by Django 4.0.4 on 2022-09-09 14:52
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("tests", "0008_modelwithstringtypeprimarykey"),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name="eventpage",
+            options={
+                "permissions": [
+                    ("custom_see_panel_setting", "Can see the panel."),
+                    ("other_custom_see_panel_setting", "Can see the panel."),
+                ]
+            },
+        ),
+    ]

+ 6 - 0
wagtail/test/testapp/models.py

@@ -407,6 +407,12 @@ class EventPage(Page):
         FieldPanel("feed_image"),
     ]
 
+    class Meta:
+        permissions = [
+            ("custom_see_panel_setting", "Can see the panel."),
+            ("other_custom_see_panel_setting", "Can see the panel."),
+        ]
+
 
 class HeadCountRelatedModelUsingPK(models.Model):
     """Related model that uses a custom primary key (pk) not id"""