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) * 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 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 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: 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: 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)) * 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: 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. - 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 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: - 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` - 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 A ``list`` or ``tuple`` of child panels
.. attribute:: MultiFieldPanel.heading .. attribute:: MultiFieldPanel.heading (optional)
A heading for the fields A heading for the fields
.. attribute:: MultiFieldPanel.help_text .. attribute:: MultiFieldPanel.help_text (optional)
Help text to be displayed against the panel. 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 ### InlinePanel
@ -103,7 +108,7 @@ Note that you can use `classname="collapsed"` to load the panel collapsed under
### FieldRowPanel ### FieldRowPanel
```{eval-rst} ```{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. 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 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 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. 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 ### 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) * 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 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 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 ### Bug fixes

Wyświetl plik

@ -430,12 +430,15 @@ class PanelGroup(Panel):
""" """
def __init__(self, children=(), *args, **kwargs): def __init__(self, children=(), *args, **kwargs):
permission = kwargs.pop("permission", None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.children = children self.children = children
self.permission = permission
def clone_kwargs(self): def clone_kwargs(self):
kwargs = super().clone_kwargs() kwargs = super().clone_kwargs()
kwargs["children"] = self.children kwargs["children"] = self.children
kwargs["permission"] = self.permission
return kwargs return kwargs
def get_form_options(self): def get_form_options(self):
@ -543,6 +546,15 @@ class PanelGroup(Panel):
return any(child.show_panel_furniture() for child in self.children) return any(child.show_panel_furniture() for child in self.children)
def is_shown(self): 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) return any(child.is_shown() for child in self.children)
@property @property

Wyświetl plik

@ -5,7 +5,7 @@ from unittest import mock
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model 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 import checks
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
from django.test import RequestFactory, TestCase, override_settings from django.test import RequestFactory, TestCase, override_settings
@ -374,6 +374,9 @@ class TestTabbedInterface(TestCase, WagtailTestUtils):
user = self.create_superuser(username="admin") user = self.create_superuser(username="admin")
self.request.user = user self.request.user = user
self.user = self.login() 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 # a custom tabbed interface for EventPage
self.event_page_tabbed_interface = TabbedInterface( self.event_page_tabbed_interface = TabbedInterface(
[ [
@ -398,6 +401,20 @@ class TestTabbedInterface(TestCase, WagtailTestUtils):
], ],
heading="Secret", 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) ).bind_to_model(EventPage)
@ -477,47 +494,101 @@ class TestTabbedInterface(TestCase, WagtailTestUtils):
event = EventPage(title="Abergavenny sheepdog trials") event = EventPage(title="Abergavenny sheepdog trials")
form = EventPageForm(instance=event) form = EventPageForm(instance=event)
# when signed in as a superuser all three tabs should be visible with self.subTest("Super user test"):
tabbed_interface = self.event_page_tabbed_interface.get_bound_panel( # when signed in as a superuser all tabs should be visible
instance=event, tabbed_interface = self.event_page_tabbed_interface.get_bound_panel(
form=form, instance=event,
request=self.request, form=form,
) request=self.request,
result = tabbed_interface.render_html() )
self.assertIn( result = tabbed_interface.render_html()
'<a id="tab-label-event_details" href="#tab-event_details" class="w-tabs__tab shiny" role="tab" aria-selected="false" tabindex="-1">', self.assertIn(
result, '<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">', self.assertIn(
result, '<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" ', self.assertIn(
result, '<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 with self.subTest("Not superuser permissions"):
user = AnonymousUser() # technically, Anonymous users cannot access the admin """
self.request.user = user The super user panel should not show, nor should the panel they dont have
tabbed_interface = self.event_page_tabbed_interface.get_bound_panel( permission for.
instance=event, """
form=form, self.request.user = self.other_user
request=self.request,
) tabbed_interface = self.event_page_tabbed_interface.get_bound_panel(
result = tabbed_interface.render_html() instance=event,
self.assertIn( form=form,
'<a id="tab-label-event_details" href="#tab-event_details" class="w-tabs__tab shiny" role="tab" aria-selected="false" tabindex="-1">', request=self.request,
result, )
) result = tabbed_interface.render_html()
self.assertIn( self.assertIn(
'<a id="tab-label-speakers" href="#tab-speakers" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">', '<a id="tab-label-event_details" href="#tab-event_details" class="w-tabs__tab shiny" role="tab" aria-selected="false" tabindex="-1">',
result, result,
) )
self.assertNotIn( self.assertIn(
'<a id="tab-label-secret" href="#tab-secret" ', '<a id="tab-label-speakers" href="#tab-speakers" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
result, 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): 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"), 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): class HeadCountRelatedModelUsingPK(models.Model):
"""Related model that uses a custom primary key (pk) not id""" """Related model that uses a custom primary key (pk) not id"""