diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f152a4d589..afb5bdfdb8 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -7,6 +7,7 @@ Changelog * Use minified versions of jQuery and jQuery UI in the admin. Total savings without compression 371 KB (Tom Dyson) * Hooks can now specify the order in which they are run (Gagaro) * Added a `submit_buttons` block to login template (Gagaro) + * Added `construct_image_chooser_queryset`, `construct_document_chooser_queryset` and `construct_page_chooser_queryset` hooks (Gagaro) * The homepage created in the project template is now titled "Home" rather than "Homepage" (Karl Hobley) * Signal receivers for custom `Image` and `Rendition` models are connected automatically (Mike Dingjan) * Fix: Marked 'Date from' / 'Date to' strings in wagtailforms for translation (Vorlif) diff --git a/docs/reference/hooks.rst b/docs/reference/hooks.rst index 87a1c64c12..7f4684e776 100644 --- a/docs/reference/hooks.rst +++ b/docs/reference/hooks.rst @@ -511,6 +511,66 @@ Hooks for customising the way users are directed through the process of creating return items.append( UserbarPuppyLinkItem() ) +Choosers +-------- + +.. _construct_page_chooser_queryset: + +``construct_page_chooser_queryset`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Called when rendering the page chooser view, to allow the page listing queryset to be customised. The callable passed into the hook will receive the current page queryset and the request object, and must return a Page queryset (either the original one, or a new one). + + .. code-block:: python + + from wagtail.wagtailcore import hooks + + @hooks.register('construct_page_chooser_queryset') + def show_my_pages_only(pages, request): + # Only show own pages + pages = pages.filter(owner=request.user) + + return pages + + +.. _construct_document_chooser_queryset: + +``construct_document_chooser_queryset`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Called when rendering the document chooser view, to allow the document listing queryset to be customised. The callable passed into the hook will receive the current document queryset and the request object, and must return a Document queryset (either the original one, or a new one). + + .. code-block:: python + + from wagtail.wagtailcore import hooks + + @hooks.register('construct_document_chooser_queryset') + def show_my_uploaded_documents_only(documents, request): + # Only show uploaded documents + documents = documents.filter(uploaded_by=request.user) + + return documents + + +.. _construct_image_chooser_queryset: + +``construct_image_chooser_queryset`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Called when rendering the image chooser view, to allow the image listing queryset to be customised. The callable passed into the hook will receive the current image queryset and the request object, and must return a Document queryset (either the original one, or a new one). + + .. code-block:: python + + from wagtail.wagtailcore import hooks + + @hooks.register('construct_image_chooser_queryset') + def show_my_uploaded_images_only(images, request): + # Only show uploaded images + images = images.filter(uploaded_by=request.user) + + return images + + Page explorer ------------- diff --git a/docs/releases/1.10.rst b/docs/releases/1.10.rst index 97b35d2d07..47eeca369a 100644 --- a/docs/releases/1.10.rst +++ b/docs/releases/1.10.rst @@ -17,6 +17,7 @@ Other features * Use minified versions of jQuery and jQuery UI in the admin. Total savings without compression 371 KB (Tom Dyson) * Hooks can now specify the order in which they are run (Gagaro) * Added a ``submit_buttons`` block to login template (Gagaro) + * Added ``construct_image_chooser_queryset``, ``construct_document_chooser_queryset`` and ``construct_page_chooser_queryset`` hooks (Gagaro) * The homepage created in the project template is now titled "Home" rather than "Homepage" (Karl Hobley) * Signal receivers for custom ``Image`` and ``Rendition`` models are connected automatically (Mike Dingjan) diff --git a/wagtail/wagtailadmin/tests/test_page_chooser.py b/wagtail/wagtailadmin/tests/test_page_chooser.py index 71796c5162..48d5c9464f 100644 --- a/wagtail/wagtailadmin/tests/test_page_chooser.py +++ b/wagtail/wagtailadmin/tests/test_page_chooser.py @@ -27,6 +27,21 @@ class TestChooserBrowse(TestCase, WagtailTestUtils): self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'wagtailadmin/chooser/browse.html') + def test_construct_queryset_hook(self): + page = SimplePage(title="Test shown", content="hello") + Page.get_first_root_node().add_child(instance=page) + + page_not_shown = SimplePage(title="Test not shown", content="hello") + Page.get_first_root_node().add_child(instance=page_not_shown) + + def filter_pages(pages, request): + return pages.filter(id=page.id) + + with self.register_hook('construct_page_chooser_queryset', filter_pages): + response = self.get() + self.assertEqual(len(response.context['pages']), 1) + self.assertEqual(response.context['pages'][0].specific, page) + class TestCanChooseRootFlag(TestCase, WagtailTestUtils): def setUp(self): @@ -297,6 +312,21 @@ class TestChooserSearch(TestCase, WagtailTestUtils): response = self.get({'page_type': 'foo'}) self.assertEqual(response.status_code, 404) + def test_construct_queryset_hook(self): + page = SimplePage(title="Test shown", content="hello") + self.root_page.add_child(instance=page) + + page_not_shown = SimplePage(title="Test not shown", content="hello") + self.root_page.add_child(instance=page_not_shown) + + def filter_pages(pages, request): + return pages.filter(id=page.id) + + with self.register_hook('construct_page_chooser_queryset', filter_pages): + response = self.get({'q': 'Test'}) + self.assertEqual(len(response.context['pages']), 1) + self.assertEqual(response.context['pages'][0].specific, page) + class TestAutomaticRootPageDetection(TestCase, WagtailTestUtils): def setUp(self): diff --git a/wagtail/wagtailadmin/views/chooser.py b/wagtail/wagtailadmin/views/chooser.py index 473a453142..fedf0a3e35 100644 --- a/wagtail/wagtailadmin/views/chooser.py +++ b/wagtail/wagtailadmin/views/chooser.py @@ -8,6 +8,7 @@ from django.shortcuts import get_object_or_404, render from wagtail.utils.pagination import paginate from wagtail.wagtailadmin.forms import EmailLinkChooserForm, ExternalLinkChooserForm, SearchForm from wagtail.wagtailadmin.modal_workflow import render_modal_workflow +from wagtail.wagtailcore import hooks from wagtail.wagtailcore.models import Page from wagtail.wagtailcore.utils import resolve_model_string @@ -75,6 +76,10 @@ def browse(request, parent_page_id=None): # Get children of parent page pages = parent_page.get_children() + # allow hooks to modify the queryset + for hook in hooks.get_hooks('construct_page_chooser_queryset'): + pages = hook(pages, request) + # Filter them by page type if desired_classes != (Page,): # restrict the page listing to just those pages that: @@ -131,15 +136,20 @@ def search(request, parent_page_id=None): except (ValueError, LookupError): raise Http404 + pages = Page.objects.all() + # allow hooks to modify the queryset + for hook in hooks.get_hooks('construct_page_chooser_queryset'): + pages = hook(pages, request) + search_form = SearchForm(request.GET) if search_form.is_valid() and search_form.cleaned_data['q']: - pages = Page.objects.exclude( + pages = pages.exclude( depth=1 # never include root ) pages = filter_page_type(pages, desired_classes) pages = pages.search(search_form.cleaned_data['q'], fields=['title']) else: - pages = Page.objects.none() + pages = pages.none() paginator, pages = paginate(request, pages, per_page=25) diff --git a/wagtail/wagtaildocs/tests.py b/wagtail/wagtaildocs/tests.py index d5d36de329..51827c4adf 100644 --- a/wagtail/wagtaildocs/tests.py +++ b/wagtail/wagtaildocs/tests.py @@ -634,7 +634,7 @@ class TestMultipleDocumentUploader(TestCase, WagtailTestUtils): class TestDocumentChooserView(TestCase, WagtailTestUtils): def setUp(self): - self.login() + self.user = self.login() def test_simple(self): response = self.client.get(reverse('wagtaildocs:chooser')) @@ -688,6 +688,44 @@ class TestDocumentChooserView(TestCase, WagtailTestUtils): # Check that we got the last page self.assertEqual(response.context['documents'].number, response.context['documents'].paginator.num_pages) + def test_construct_queryset_hook_browse(self): + document = Document.objects.create( + title="Test document shown", + uploaded_by_user=self.user, + ) + Document.objects.create( + title="Test document not shown", + ) + + def filter_documents(documents, request): + # Filter on `uploaded_by_user` because it is + # the only default FilterField in search_fields + return documents.filter(uploaded_by_user=self.user) + + with self.register_hook('construct_document_chooser_queryset', filter_documents): + response = self.client.get(reverse('wagtaildocs:chooser')) + self.assertEqual(len(response.context['documents']), 1) + self.assertEqual(response.context['documents'][0], document) + + def test_construct_queryset_hook_search(self): + document = Document.objects.create( + title="Test document shown", + uploaded_by_user=self.user, + ) + Document.objects.create( + title="Test document not shown", + ) + + def filter_documents(documents, request): + # Filter on `uploaded_by_user` because it is + # the only default FilterField in search_fields + return documents.filter(uploaded_by_user=self.user) + + with self.register_hook('construct_document_chooser_queryset', filter_documents): + response = self.client.get(reverse('wagtaildocs:chooser'), {'q': 'Test'}) + self.assertEqual(len(response.context['documents']), 1) + self.assertEqual(response.context['documents'][0], document) + class TestDocumentChooserChosenView(TestCase, WagtailTestUtils): def setUp(self): diff --git a/wagtail/wagtaildocs/views/chooser.py b/wagtail/wagtaildocs/views/chooser.py index 0e86936835..7d10c5ef4f 100644 --- a/wagtail/wagtaildocs/views/chooser.py +++ b/wagtail/wagtaildocs/views/chooser.py @@ -9,6 +9,7 @@ from wagtail.utils.pagination import paginate from wagtail.wagtailadmin.forms import SearchForm from wagtail.wagtailadmin.modal_workflow import render_modal_workflow from wagtail.wagtailadmin.utils import PermissionPolicyChecker +from wagtail.wagtailcore import hooks from wagtail.wagtailcore.models import Collection from wagtail.wagtaildocs.forms import get_document_form from wagtail.wagtaildocs.models import get_document_model @@ -41,12 +42,14 @@ def chooser(request): else: uploadform = None - documents = [] + documents = Document.objects.all() + + # allow hooks to modify the queryset + for hook in hooks.get_hooks('construct_document_chooser_queryset'): + documents = hook(documents, request) q = None - is_searching = False if 'q' in request.GET or 'p' in request.GET or 'collection_id' in request.GET: - documents = Document.objects.all() collection_id = request.GET.get('collection_id') if collection_id: @@ -77,16 +80,16 @@ def chooser(request): if len(collections) < 2: collections = None - documents = Document.objects.order_by('-created_at') + documents = documents.order_by('-created_at') paginator, documents = paginate(request, documents, per_page=10) - return render_modal_workflow(request, 'wagtaildocs/chooser/chooser.html', 'wagtaildocs/chooser/chooser.js', { - 'documents': documents, - 'uploadform': uploadform, - 'searchform': searchform, - 'collections': collections, - 'is_searching': False, - }) + return render_modal_workflow(request, 'wagtaildocs/chooser/chooser.html', 'wagtaildocs/chooser/chooser.js', { + 'documents': documents, + 'uploadform': uploadform, + 'searchform': searchform, + 'collections': collections, + 'is_searching': False, + }) def document_chosen(request, document_id): diff --git a/wagtail/wagtailimages/tests/test_admin_views.py b/wagtail/wagtailimages/tests/test_admin_views.py index cb83529838..3b73fd7e66 100644 --- a/wagtail/wagtailimages/tests/test_admin_views.py +++ b/wagtail/wagtailimages/tests/test_admin_views.py @@ -412,7 +412,7 @@ class TestImageDeleteView(TestCase, WagtailTestUtils): class TestImageChooserView(TestCase, WagtailTestUtils): def setUp(self): - self.login() + self.user = self.login() def get(self, params={}): return self.client.get(reverse('wagtailimages:chooser'), params) @@ -452,6 +452,48 @@ class TestImageChooserView(TestCase, WagtailTestUtils): # Results should not include images that just have 'even' in the title self.assertNotContains(response, "Test image 3 is even better") + def test_construct_queryset_hook_browse(self): + image = Image.objects.create( + title="Test image shown", + file=get_test_image_file(), + uploaded_by_user=self.user, + ) + Image.objects.create( + title="Test image not shown", + file=get_test_image_file(), + ) + + def filter_images(images, request): + # Filter on `uploaded_by_user` because it is + # the only default FilterField in search_fields + return images.filter(uploaded_by_user=self.user) + + with self.register_hook('construct_image_chooser_queryset', filter_images): + response = self.get() + self.assertEqual(len(response.context['images']), 1) + self.assertEqual(response.context['images'][0], image) + + def test_construct_queryset_hook_search(self): + image = Image.objects.create( + title="Test image shown", + file=get_test_image_file(), + uploaded_by_user=self.user, + ) + Image.objects.create( + title="Test image not shown", + file=get_test_image_file(), + ) + + def filter_images(images, request): + # Filter on `uploaded_by_user` because it is + # the only default FilterField in search_fields + return images.filter(uploaded_by_user=self.user) + + with self.register_hook('construct_image_chooser_queryset', filter_images): + response = self.get({'q': 'Test'}) + self.assertEqual(len(response.context['images']), 1) + self.assertEqual(response.context['images'][0], image) + class TestImageChooserChosenView(TestCase, WagtailTestUtils): def setUp(self): diff --git a/wagtail/wagtailimages/views/chooser.py b/wagtail/wagtailimages/views/chooser.py index b732c3acc0..ef51ef767c 100644 --- a/wagtail/wagtailimages/views/chooser.py +++ b/wagtail/wagtailimages/views/chooser.py @@ -9,6 +9,7 @@ from wagtail.utils.pagination import paginate from wagtail.wagtailadmin.forms import SearchForm from wagtail.wagtailadmin.modal_workflow import render_modal_workflow from wagtail.wagtailadmin.utils import PermissionPolicyChecker, popular_tags_for_model +from wagtail.wagtailcore import hooks from wagtail.wagtailcore.models import Collection from wagtail.wagtailimages import get_image_model from wagtail.wagtailimages.formats import get_image_format @@ -49,6 +50,10 @@ def chooser(request): images = Image.objects.order_by('-created_at') + # allow hooks to modify the queryset + for hook in hooks.get_hooks('construct_image_chooser_queryset'): + images = hook(images, request) + q = None if ( 'q' in request.GET or 'p' in request.GET or 'tag' in request.GET or @@ -91,16 +96,16 @@ def chooser(request): paginator, images = paginate(request, images, per_page=12) - return render_modal_workflow(request, 'wagtailimages/chooser/chooser.html', 'wagtailimages/chooser/chooser.js', { - 'images': images, - 'uploadform': uploadform, - 'searchform': searchform, - 'is_searching': False, - 'query_string': q, - 'will_select_format': request.GET.get('select_format'), - 'popular_tags': popular_tags_for_model(Image), - 'collections': collections, - }) + return render_modal_workflow(request, 'wagtailimages/chooser/chooser.html', 'wagtailimages/chooser/chooser.js', { + 'images': images, + 'uploadform': uploadform, + 'searchform': searchform, + 'is_searching': False, + 'query_string': q, + 'will_select_format': request.GET.get('select_format'), + 'popular_tags': popular_tags_for_model(Image), + 'collections': collections, + }) def image_chosen(request, image_id):