Add ability to check permission on parent `PanelGroup` class

- Used by TabbedInterface, ObjectList, FieldRowPanel, MultiFieldPanel
pull/9414/head
Oliver Parker 2022-09-16 08:01:24 +01:00 zatwierdzone przez LB (Ben Johnston)
rodzic 55f42d29c8
commit 19fd2ceb98
8 zmienionych plików z 170 dodań i 46 usunięć

Wyświetl plik

@ -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))

Wyświetl plik

@ -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`

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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,7 +494,8 @@ 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
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,
@ -496,9 +514,54 @@ class TestTabbedInterface(TestCase, WagtailTestUtils):
'<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,
)
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
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,
@ -518,6 +581,14 @@ class TestTabbedInterface(TestCase, WagtailTestUtils):
'<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):

Wyświetl plik

@ -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."),
]
},
),
]

Wyświetl plik

@ -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"""