kopia lustrzana https://github.com/wagtail/wagtail
Add ability to check permission on parent `PanelGroup` class
- Used by TabbedInterface, ObjectList, FieldRowPanel, MultiFieldPanelpull/9414/head
rodzic
55f42d29c8
commit
19fd2ceb98
|
@ -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))
|
||||||
|
|
|
@ -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`
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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."),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -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"""
|
||||||
|
|
Ładowanie…
Reference in New Issue