diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8efc46004..f4e5a11d9 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -13,6 +13,7 @@ Changelog * Usage count now shows on delete confirmation page when WAGTAIL_USAGE_COUNT_ENABLED is active (Kees Hink) * Added usage count to snippets (Kees Hink) * Moved usage count to the sidebar on the edit page (Kees Hink) + * Explorer menu now reflects customisations to the page listing made via the `construct_explorer_page_queryset` hook and `ModelAdmin.exclude_from_explorer` property (Tim Heap) * Fix: Do not remove stopwords when generating slugs from non-ASCII titles, to avoid issues with incorrect word boundaries (Sævar Öfjörð Magnússon) * Fix: The PostgreSQL search backend now preserves ordering of the `QuerySet` when searching with `order_by_relevance=False` (Bertrand Bordage) * Fix: Using `modeladmin_register` as a decorator no longer replaces the decorated class with `None` (Tim Heap) diff --git a/client/src/api/admin.js b/client/src/api/admin.js index ec302fb01..d299d882b 100644 --- a/client/src/api/admin.js +++ b/client/src/api/admin.js @@ -10,7 +10,7 @@ export const getPage = (id) => { }; export const getPageChildren = (id, options = {}) => { - let url = `${ADMIN_API.PAGES}?child_of=${id}`; + let url = `${ADMIN_API.PAGES}?child_of=${id}&for_explorer=1`; if (options.fields) { url += `&fields=${global.encodeURIComponent(options.fields.join(','))}`; diff --git a/client/src/api/admin.test.js b/client/src/api/admin.test.js index 64eff5a80..dce166806 100644 --- a/client/src/api/admin.test.js +++ b/client/src/api/admin.test.js @@ -20,23 +20,23 @@ describe('admin API', () => { describe('getPageChildren', () => { it('works', () => { getPageChildren(3); - expect(client.get).toBeCalledWith(`${ADMIN_API.PAGES}?child_of=3`); + expect(client.get).toBeCalledWith(`${ADMIN_API.PAGES}?child_of=3&for_explorer=1`); }); it('#fields', () => { getPageChildren(3, { fields: ['title', 'latest_revision_created_at'] }); // eslint-disable-next-line max-len - expect(client.get).toBeCalledWith(`${ADMIN_API.PAGES}?child_of=3&fields=title%2Clatest_revision_created_at`); + expect(client.get).toBeCalledWith(`${ADMIN_API.PAGES}?child_of=3&for_explorer=1&fields=title%2Clatest_revision_created_at`); }); it('#onlyWithChildren', () => { getPageChildren(3, { onlyWithChildren: true }); - expect(client.get).toBeCalledWith(`${ADMIN_API.PAGES}?child_of=3&has_children=1`); + expect(client.get).toBeCalledWith(`${ADMIN_API.PAGES}?child_of=3&for_explorer=1&has_children=1`); }); it('#offset', () => { getPageChildren(3, { offset: 5 }); - expect(client.get).toBeCalledWith(`${ADMIN_API.PAGES}?child_of=3&offset=5`); + expect(client.get).toBeCalledWith(`${ADMIN_API.PAGES}?child_of=3&for_explorer=1&offset=5`); }); }); diff --git a/docs/releases/2.0.rst b/docs/releases/2.0.rst index 559a0f34d..f5e90cb9a 100644 --- a/docs/releases/2.0.rst +++ b/docs/releases/2.0.rst @@ -21,6 +21,7 @@ Other features * Usage count now shows on delete confirmation page when WAGTAIL_USAGE_COUNT_ENABLED is active (Kees Hink) * Added usage count to snippets (Kees Hink) * Moved usage count to the sidebar on the edit page (Kees Hink) + * Explorer menu now reflects customisations to the page listing made via the `construct_explorer_page_queryset` hook and `ModelAdmin.exclude_from_explorer` property (Tim Heap) Bug fixes ~~~~~~~~~ diff --git a/wagtail/api/v2/filters.py b/wagtail/api/v2/filters.py index 073eb060b..0e0a554ff 100644 --- a/wagtail/api/v2/filters.py +++ b/wagtail/api/v2/filters.py @@ -5,6 +5,7 @@ from django.db import models from rest_framework.filters import BaseFilterBackend from taggit.managers import TaggableManager +from wagtail.wagtailcore import hooks from wagtail.wagtailcore.models import Page from wagtail.wagtailsearch.backends import get_search_backend @@ -150,7 +151,7 @@ class ChildOfFilter(BaseFilterBackend): raise BadRequestError("parent page doesn't exist") queryset = queryset.child_of(parent_page) - queryset._filtered_by_child_of = True + queryset._filtered_by_child_of = parent_page return queryset @@ -181,7 +182,7 @@ class DescendantOfFilter(BaseFilterBackend): def filter_queryset(self, request, queryset, view): if 'descendant_of' in request.GET: - if getattr(queryset, '_filtered_by_child_of', False): + if hasattr(queryset, '_filtered_by_child_of'): raise BadRequestError("filtering by descendant_of with child_of is not supported") try: parent_page_id = int(request.GET['descendant_of']) @@ -212,3 +213,16 @@ class RestrictedDescendantOfFilter(DescendantOfFilter): def get_page_by_id(self, request, page_id): site_pages = pages_for_site(request.site) return site_pages.get(id=page_id) + + +class ForExplorerFilter(BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + if request.GET.get('for_explorer'): + if not hasattr(queryset, '_filtered_by_child_of'): + raise BadRequestError("filtering by for_explorer without child_of is not supported") + + parent_page = queryset._filtered_by_child_of + for hook in hooks.get_hooks('construct_explorer_page_queryset'): + queryset = hook(parent_page, queryset, request) + + return queryset diff --git a/wagtail/tests/testapp/wagtail_hooks.py b/wagtail/tests/testapp/wagtail_hooks.py index 19639e1c7..41b4d1686 100644 --- a/wagtail/tests/testapp/wagtail_hooks.py +++ b/wagtail/tests/testapp/wagtail_hooks.py @@ -93,6 +93,12 @@ def polite_pages_only(parent_page, pages, request): return pages +@hooks.register('construct_explorer_page_queryset') +def hide_hidden_pages(parent_page, pages, request): + # Pages with 'hidden' in their title are hidden. Magic! + return pages.exclude(title__icontains='hidden') + + # register 'blockquote' as a rich text feature supported by a hallo.js plugin @hooks.register('register_rich_text_features') def register_blockquote_feature(features): diff --git a/wagtail/wagtailadmin/api/endpoints.py b/wagtail/wagtailadmin/api/endpoints.py index e5d201d61..0f0a8a7c4 100644 --- a/wagtail/wagtailadmin/api/endpoints.py +++ b/wagtail/wagtailadmin/api/endpoints.py @@ -4,7 +4,8 @@ from collections import OrderedDict from wagtail.api.v2.endpoints import PagesAPIEndpoint from wagtail.api.v2.filters import ( - ChildOfFilter, DescendantOfFilter, FieldsFilter, OrderingFilter, SearchFilter) + ChildOfFilter, DescendantOfFilter, FieldsFilter, ForExplorerFilter, OrderingFilter, + SearchFilter) from wagtail.api.v2.utils import BadRequestError, filter_page_type, page_models_from_string from wagtail.wagtailcore.models import Page @@ -21,6 +22,7 @@ class PagesAdminAPIEndpoint(PagesAPIEndpoint): FieldsFilter, ChildOfFilter, DescendantOfFilter, + ForExplorerFilter, HasChildrenFilter, OrderingFilter, SearchFilter, @@ -49,6 +51,7 @@ class PagesAdminAPIEndpoint(PagesAPIEndpoint): detail_only_fields = [] known_query_parameters = PagesAPIEndpoint.known_query_parameters.union([ + 'for_explorer', 'has_children' ]) diff --git a/wagtail/wagtailadmin/tests/api/test_pages.py b/wagtail/wagtailadmin/tests/api/test_pages.py index b8c3345fb..3801fd7a1 100644 --- a/wagtail/wagtailadmin/tests/api/test_pages.py +++ b/wagtail/wagtailadmin/tests/api/test_pages.py @@ -9,7 +9,7 @@ from django.utils import timezone from wagtail.api.v2.tests.test_pages import TestPageDetail, TestPageListing from wagtail.tests.demosite import models -from wagtail.tests.testapp.models import StreamPage +from wagtail.tests.testapp.models import SimplePage, StreamPage from wagtail.wagtailcore.models import Page from .utils import AdminAPITestCase @@ -301,6 +301,42 @@ class TestAdminPageListing(AdminAPITestCase, TestPageListing): self.assertEqual(response.status_code, 200) + # FOR EXPLORER FILTER + + def make_simple_page(self, parent, title): + return parent.add_child(instance=SimplePage(title=title, content='Simple page')) + + def test_for_explorer_filter(self): + movies = self.make_simple_page(Page.objects.get(pk=1), 'Movies') + visible_movies = [ + self.make_simple_page(movies, 'The Way of the Dragon'), + self.make_simple_page(movies, 'Enter the Dragon'), + self.make_simple_page(movies, 'Dragons Forever'), + ] + hidden_movies = [ + self.make_simple_page(movies, 'The Hidden Fortress'), + self.make_simple_page(movies, 'Crouching Tiger, Hidden Dragon'), + self.make_simple_page(movies, 'Crouching Tiger, Hidden Dragon: Sword of Destiny'), + ] + + response = self.get_response(child_of=movies.pk, for_explorer=1) + content = json.loads(response.content.decode('UTF-8')) + page_id_list = self.get_page_id_list(content) + self.assertEqual(page_id_list, [page.pk for page in visible_movies]) + + response = self.get_response(child_of=movies.pk) + content = json.loads(response.content.decode('UTF-8')) + page_id_list = self.get_page_id_list(content) + self.assertEqual(page_id_list, [page.pk for page in visible_movies + hidden_movies]) + + def test_for_explorer_no_child_of(self): + response = self.get_response(for_explorer=1) + self.assertEqual(response.status_code, 400) + content = json.loads(response.content.decode('UTF-8')) + self.assertEqual(content, { + 'message': 'filtering by for_explorer without child_of is not supported', + }) + # HAS CHILDREN FILTER def test_has_children_filter(self):