diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 1b28c27d08..1b277e99f8 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -918,3 +918,19 @@ WAGTAIL_WORKFLOW_CANCEL_ON_PUBLISH = True This determines whether publishing a page with an ongoing workflow will cancel the workflow (if true) or leave the workflow unaffected (false). Disabling this could be useful if your site has long, multi-step workflows, and you want to be able to publish urgent page updates while the workflow continues to provide less urgent feedback. + +## Snippets + +(wagtailsnippets_menu_show_all)= + +### `WAGTAILSNIPPETS_MENU_SHOW_ALL` + +```python +WAGTAILSNIPPETS_MENU_SHOW_ALL = False +``` + +The sidebar "Snippets" menu item is only shown if any snippet models exist +[without their own menu items](wagtailsnippets_menu_item) +and by default its view only contains those models. +This setting can be set to `True` to always show the "Snippets" menu item +and to have its view include all snippet models for which the user has permission to add, view, or change. diff --git a/docs/releases/7.0.md b/docs/releases/7.0.md index 6943a02c89..8ca50d7b0c 100644 --- a/docs/releases/7.0.md +++ b/docs/releases/7.0.md @@ -181,6 +181,14 @@ This option is enabled as standard for the `title` field of page models. It is a In previous releases, the `save()` method on page models called the `full_clean` method to apply [model-level validation rules](inv:django#validating-objects), regardless of whether the page was in a draft or live state, unless this was explicitly disabled by passing `clean=False`. As of this release, saving a page in a draft state (`live=False`) will only perform the minimum validation necessary to ensure data integrity: the title must be non-empty, and the slug must be unique within the parent page. Saving a page with `live=True` will apply full validation as before. If you have user code that creates draft pages and requires them to be validated, you must now call `full_clean` explicitly. +### "Snippets" menu now only includes snippet models without menu items + +The "Snippets" sidebar menu item appears if there are snippet models [without their own menu items](wagtailsnippets_menu_item). +Previously, the "Snippets" menu item pointed to a snippets index view that listed all snippet models whether they'd been configured with their own menu items or not. +This behaviour has been changed and the snippets index view will now only include snippet models that haven't been configured that way. + +The new [](wagtailsnippets_menu_show_all) setting can be used to always show a top-level "Snippets" menu item in the sidebar pointing to an index view that includes all snippet models. + ## Upgrade considerations - deprecation of old functionality ### `TAG_LIMIT` and `TAG_SPACES_ALLOWED` settings renamed to `WAGTAIL_TAG_LIMIT` and `WAGTAIL_TAG_SPACES_ALLOWED` diff --git a/docs/topics/snippets/customizing.md b/docs/topics/snippets/customizing.md index 05f2f5fc70..4306d3609e 100644 --- a/docs/topics/snippets/customizing.md +++ b/docs/topics/snippets/customizing.md @@ -108,6 +108,8 @@ The inspect view is disabled by default, as it's not often useful for most model Template customizations work the same way as for `ModelViewSet`, except that the {attr}`~.ModelViewSet.template_prefix` defaults to `wagtailsnippets/snippets/`. Refer to [the template customizations for `ModelViewSet`](modelviewset_templates) for more details. +(wagtailsnippets_menu_item)= + ## Menu item By default, registering a snippet model will add a "Snippets" menu item to the sidebar menu. However, you can configure a snippet model to have its own top-level menu item in the sidebar menu by setting {attr}`~.ViewSet.add_to_admin_menu` to `True`. Refer to [the menu customizations for `ModelViewSet`](modelviewset_menu) for more details. @@ -160,6 +162,8 @@ class MarketingViewSetGroup(SnippetViewSetGroup): register_snippet(MarketingViewSetGroup) ``` +By default, the sidebar "Snippets" menu item will only show snippet models that haven't been configured with their own menu items. If all snippet models have their own menu items, the "Snippets" menu item will not be shown. +This behaviour can be changed using the [](wagtailsnippets_menu_show_all) setting. Various additional attributes are available to customize the viewset - see {class}`~SnippetViewSet`. diff --git a/wagtail/snippets/permissions.py b/wagtail/snippets/permissions.py index f72bb0ec3d..f0f4eb8f8d 100644 --- a/wagtail/snippets/permissions.py +++ b/wagtail/snippets/permissions.py @@ -19,14 +19,16 @@ def user_can_edit_snippet_type(user, model): return False -def user_can_access_snippets(user): +def user_can_access_snippets(user, models=None): """ true if user has 'add', 'change', 'delete', or 'view' permission - on any model registered as a snippet type + on any model registered as a snippet type - or if a `models` list + is passed, any of those models """ - snippet_models = get_snippet_models() + if models is None: + models = get_snippet_models() - for model in snippet_models: + for model in models: if model.snippet_viewset.permission_policy.user_has_any_permission( user, {"add", "change", "delete", "view"} ): diff --git a/wagtail/snippets/tests/test_snippets.py b/wagtail/snippets/tests/test_snippets.py index 7bfa9a252a..66b9a9f73d 100644 --- a/wagtail/snippets/tests/test_snippets.py +++ b/wagtail/snippets/tests/test_snippets.py @@ -14,7 +14,7 @@ from django.core.files.base import ContentFile from django.core.files.uploadedfile import SimpleUploadedFile from django.core.handlers.wsgi import WSGIRequest from django.http import HttpRequest, HttpResponse -from django.test import RequestFactory, TestCase, TransactionTestCase +from django.test import RequestFactory, SimpleTestCase, TestCase, TransactionTestCase from django.test.utils import override_settings from django.urls import reverse from django.utils.timezone import make_aware, now @@ -38,6 +38,7 @@ from wagtail.snippets.action_menu import ( ) from wagtail.snippets.blocks import SnippetChooserBlock from wagtail.snippets.models import SNIPPET_MODELS, register_snippet +from wagtail.snippets.views.snippets import get_snippet_models_for_index_view from wagtail.snippets.widgets import ( AdminSnippetChooser, SnippetChooserAdapter, @@ -80,6 +81,22 @@ from wagtail.test.utils.timestamps import submittable_timestamp from wagtail.utils.timestamps import render_timestamp +class TestGetSnippetModelsForIndexView(SimpleTestCase): + def test_default_lists_all_snippets_without_menu_items(self): + self.assertEqual( + get_snippet_models_for_index_view(), + [ + model + for model in SNIPPET_MODELS + if not model.snippet_viewset.get_menu_item_is_registered() + ], + ) + + @override_settings(WAGTAILSNIPPETS_MENU_SHOW_ALL=True) + def test_setting_allows_listing_of_all_snippet_models(self): + self.assertEqual(get_snippet_models_for_index_view(), SNIPPET_MODELS) + + class TestSnippetIndexView(AdminTemplateTestUtils, WagtailTestUtils, TestCase): def setUp(self): self.user = self.login() diff --git a/wagtail/snippets/tests/test_viewset.py b/wagtail/snippets/tests/test_viewset.py index 2a16a4fcde..7a7c7b5b17 100644 --- a/wagtail/snippets/tests/test_viewset.py +++ b/wagtail/snippets/tests/test_viewset.py @@ -1204,7 +1204,7 @@ class TestMenuItemRegistration(BaseSnippetViewSetTests): self.assertEqual(item.url, reverse("wagtailsnippets:index")) # Clear cached property - del item._all_have_menu_items + del item._snippets_in_index_view with mock.patch( "wagtail.snippets.views.snippets.SnippetViewSet.get_menu_item_is_registered" @@ -1214,6 +1214,19 @@ class TestMenuItemRegistration(BaseSnippetViewSetTests): snippets = [item for item in menu_items if item.name == "snippets"] self.assertEqual(len(snippets), 0) + def test_snippets_menu_item_hidden_when_user_lacks_permissions_for_snippets(self): + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + menu_items = admin_menu.render_component(self.request) + snippets = [item for item in menu_items if item.name == "snippets"] + self.assertEqual(len(snippets), 0) + class TestCustomFormClass(BaseSnippetViewSetTests): model = DraftStateModel diff --git a/wagtail/snippets/views/snippets.py b/wagtail/snippets/views/snippets.py index dd4c294cd1..57fc1e99ef 100644 --- a/wagtail/snippets/views/snippets.py +++ b/wagtail/snippets/views/snippets.py @@ -1,4 +1,5 @@ from django.apps import apps +from django.conf import settings from django.contrib.admin.utils import quote from django.core import checks from django.core.exceptions import ImproperlyConfigured, PermissionDenied @@ -68,6 +69,19 @@ def get_snippet_model_from_url_params(app_name, model_name): # == Views == +def get_snippet_models_for_index_view(): + models = get_snippet_models() + + if getattr(settings, "WAGTAILSNIPPETS_MENU_SHOW_ALL", False): + return models + + return [ + model + for model in models + if not model.snippet_viewset.get_menu_item_is_registered() + ] + + class ModelIndexView(generic.BaseListingView): page_title = gettext_lazy("Snippets") header_icon = "snippet" @@ -86,7 +100,7 @@ class ModelIndexView(generic.BaseListingView): "model": model, "url": url, } - for model in get_snippet_models() + for model in get_snippet_models_for_index_view() if (url := self.get_list_url(model)) ] diff --git a/wagtail/snippets/wagtail_hooks.py b/wagtail/snippets/wagtail_hooks.py index 7b955c0136..9834c61b3f 100644 --- a/wagtail/snippets/wagtail_hooks.py +++ b/wagtail/snippets/wagtail_hooks.py @@ -5,7 +5,6 @@ from django.utils.translation import gettext_lazy as _ from wagtail import hooks from wagtail.admin.menu import MenuItem from wagtail.snippets.bulk_actions.delete import DeleteBulkAction -from wagtail.snippets.models import get_snippet_models from wagtail.snippets.permissions import user_can_access_snippets from wagtail.snippets.views import snippets as snippet_views @@ -26,14 +25,11 @@ def register_admin_urls(): class SnippetsMenuItem(MenuItem): @cached_property - def _all_have_menu_items(self): - return all( - model.snippet_viewset.get_menu_item_is_registered() - for model in get_snippet_models() - ) + def _snippets_in_index_view(self): + return snippet_views.get_snippet_models_for_index_view() def is_shown(self, request): - return not self._all_have_menu_items and user_can_access_snippets(request.user) + return user_can_access_snippets(request.user, self._snippets_in_index_view) @hooks.register("register_admin_menu_item")