Filter pop-out explorer pages using 'construct_explorer_page_queryset'

The pages listed when browsing the /admin/pages/ explorer differed from
the pages listed in the new React/admin API powered pop-out explorer.
The latter did not pass the queryset through the
'construct_explorer_page_queryset' hook, so pages that should have been
hidden were visible. Visiting these pages in the explorer could then
lead to unexpected behaviours, as hidden sections of the site became
browsable.

A new `for_explorer=1` query parameter has been added to the admin API,
which will pass the page queryset through the
'construct_explorer_page_queryset' hooks.
pull/3984/merge
Tim Heap 2017-10-31 11:03:30 +11:00 zatwierdzone przez Matt Westcott
rodzic e1fc584f1c
commit 1f2b8ccaf1
8 zmienionych plików z 70 dodań i 9 usunięć

Wyświetl plik

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

Wyświetl plik

@ -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(','))}`;

Wyświetl plik

@ -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`);
});
});

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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