Repository: wagtail/wagtail

Issue:
Paginate chooser modal page listings like standard page listing
The standard explorer listing is paginated at 50 items to prevent particularly huge sections of the tree (e.g where a bulk import has taken place) from trying to render X-thousand results on one page.

The page chooser modal however is not paginated, but adding pagination is limited by what the pagination include can do (See https://github.com/torchbox/wagtail/issues/507) as it can't support the `page_type` query parameter.



Discussion:
A client site currently has 1916 pages under a particular section. It's taking a very long time to load. It would be good if we could address this in the near future.


Resolving patch:
diff --git a/wagtail/wagtailadmin/urls.py b/wagtail/wagtailadmin/urls.py
--- a/wagtail/wagtailadmin/urls.py
+++ b/wagtail/wagtailadmin/urls.py
@@ -54,6 +54,7 @@
 
     url(r'^choose-page/$', chooser.browse, name='wagtailadmin_choose_page'),
     url(r'^choose-page/(\d+)/$', chooser.browse, name='wagtailadmin_choose_page_child'),
+    url(r'^choose-page/search/$', chooser.search, name='wagtailadmin_choose_page_search'),
     url(r'^choose-external-link/$', chooser.external_link, name='wagtailadmin_choose_page_external_link'),
     url(r'^choose-email-link/$', chooser.email_link, name='wagtailadmin_choose_page_email_link'),
 
diff --git a/wagtail/wagtailadmin/views/chooser.py b/wagtail/wagtailadmin/views/chooser.py
--- a/wagtail/wagtailadmin/views/chooser.py
+++ b/wagtail/wagtailadmin/views/chooser.py
@@ -1,3 +1,4 @@
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
 from django.contrib.contenttypes.models import ContentType
 from django.shortcuts import get_object_or_404, render
 from django.http import Http404
@@ -19,68 +20,105 @@ def get_querystring(request):
 
 
 def browse(request, parent_page_id=None):
+    ITEMS_PER_PAGE = 25
+
     page_type = request.GET.get('page_type') or 'wagtailcore.page'
     content_type_app_name, content_type_model_name = page_type.split('.')
 
-    is_searching = False
-    page_types_restricted = page_type != 'wagtailcore.page'
-
     try:
         content_type = ContentType.objects.get_by_natural_key(content_type_app_name, content_type_model_name)
     except ContentType.DoesNotExist:
         raise Http404
     desired_class = content_type.model_class()
 
-    if 'q' in request.GET:
-        search_form = SearchForm(request.GET)
-        if search_form.is_valid() and search_form.cleaned_data['q']:
-            pages = desired_class.objects.exclude(
-                depth=1  # never include root
-            ).filter(title__icontains=search_form.cleaned_data['q'])[:10]
-            is_searching = True
-
-    if not is_searching:
-        if parent_page_id:
-            parent_page = get_object_or_404(Page, id=parent_page_id)
-        else:
-            parent_page = Page.get_first_root_node()
-
-        parent_page.can_choose = issubclass(parent_page.specific_class, desired_class)
-        search_form = SearchForm()
-        pages = parent_page.get_children()
-
-    # restrict the page listing to just those pages that:
-    # - are of the given content type (taking into account class inheritance)
-    # - or can be navigated into (i.e. have children)
-
-    shown_pages = []
-    for page in pages:
-        page.can_choose = issubclass(page.specific_class or Page, desired_class)
-        page.can_descend = page.get_children_count()
-
-        if page.can_choose or page.can_descend:
-            shown_pages.append(page)
+    if parent_page_id:
+        parent_page = get_object_or_404(Page, id=parent_page_id)
+    else:
+        parent_page = Page.get_first_root_node()
+
+    parent_page.can_choose = issubclass(parent_page.specific_class, desired_class)
+    search_form = SearchForm()
+    pages = parent_page.get_children()
+
+    if desired_class == Page:
+        # apply pagination first, since we know that the page listing won't
+        # have to be filtered, and that saves us walking the entire list
+        p = request.GET.get('p', 1)
+        paginator = Paginator(pages, ITEMS_PER_PAGE)
+        try:
+            pages = paginator.page(p)
+        except PageNotAnInteger:
+            pages = paginator.page(1)
+        except EmptyPage:
+            pages = paginator.page(paginator.num_pages)
+
+        for page in pages:
+            page.can_choose = True
+            page.can_descend = page.get_children_count()
 
-    if is_searching:
-        return render(request, 'wagtailadmin/chooser/_search_results.html', {
-            'querystring': get_querystring(request),
-            'searchform': search_form,
-            'pages': shown_pages,
-            'page_type_string': page_type,
-            'page_type': desired_class,
-            'page_types_restricted': page_types_restricted
-        })
+    else:
+        # restrict the page listing to just those pages that:
+        # - are of the given content type (taking into account class inheritance)
+        # - or can be navigated into (i.e. have children)
+
+        shown_pages = []
+        for page in pages:
+            page.can_choose = issubclass(page.specific_class or Page, desired_class)
+            page.can_descend = page.get_children_count()
+
+            if page.can_choose or page.can_descend:
+                shown_pages.append(page)
+
+        # Apply pagination
+        p = request.GET.get('p', 1)
+        paginator = Paginator(shown_pages, ITEMS_PER_PAGE)
+        try:
+            pages = paginator.page(p)
+        except PageNotAnInteger:
+            pages = paginator.page(1)
+        except EmptyPage:
+            pages = paginator.page(paginator.num_pages)
 
     return render_modal_workflow(request, 'wagtailadmin/chooser/browse.html', 'wagtailadmin/chooser/browse.js', {
         'allow_external_link': request.GET.get('allow_external_link'),
         'allow_email_link': request.GET.get('allow_email_link'),
         'querystring': get_querystring(request),
         'parent_page': parent_page,
-        'pages': shown_pages,
+        'pages': pages,
         'search_form': search_form,
         'page_type_string': page_type,
-        'page_type': desired_class,
-        'page_types_restricted': page_types_restricted
+        'page_type_name': desired_class.get_verbose_name(),
+        'page_types_restricted': (page_type != 'wagtailcore.page')
+    })
+
+
+def search(request, parent_page_id=None):
+    page_type = request.GET.get('page_type') or 'wagtailcore.page'
+    content_type_app_name, content_type_model_name = page_type.split('.')
+
+    try:
+        content_type = ContentType.objects.get_by_natural_key(content_type_app_name, content_type_model_name)
+    except ContentType.DoesNotExist:
+        raise Http404
+    desired_class = content_type.model_class()
+
+    search_form = SearchForm(request.GET)
+    if search_form.is_valid() and search_form.cleaned_data['q']:
+        pages = desired_class.objects.exclude(
+            depth=1  # never include root
+        ).filter(title__icontains=search_form.cleaned_data['q'])[:10]
+    else:
+        pages = desired_class.objects.none()
+
+    shown_pages = []
+    for page in pages:
+        page.can_choose = True
+        shown_pages.append(page)
+
+    return render(request, 'wagtailadmin/chooser/_search_results.html', {
+        'querystring': get_querystring(request),
+        'searchform': search_form,
+        'pages': shown_pages,
     })
 
 
diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py
--- a/wagtail/wagtailadmin/views/pages.py
+++ b/wagtail/wagtailadmin/views/pages.py
@@ -61,6 +61,7 @@ def index(request, parent_page_id=None):
     return render(request, 'wagtailadmin/pages/index.html', {
         'parent_page': parent_page,
         'ordering': ordering,
+        'pagination_query_params': "ordering=%s" % ordering,
         'pages': pages,
     })
 
@@ -683,7 +684,7 @@ def get_page_edit_handler(page_class):
 def search(request):
     pages = []
     q = None
-    is_searching = False
+
     if 'q' in request.GET:
         form = SearchForm(request.GET)
         if form.is_valid():
@@ -691,7 +692,6 @@ def search(request):
 
             # page number
             p = request.GET.get("p", 1)
-            is_searching = True
             pages = Page.search(q, show_unpublished=True, search_title_only=True, prefetch_related=['content_type'])
 
             # Pagination
@@ -708,15 +708,15 @@ def search(request):
     if request.is_ajax():
         return render(request, "wagtailadmin/pages/search_results.html", {
             'pages': pages,
-            'is_searching': is_searching,
             'query_string': q,
+            'pagination_query_params': ('q=%s' % q) if q else ''
         })
     else:
         return render(request, "wagtailadmin/pages/search.html", {
             'search_form': form,
             'pages': pages,
-            'is_searching': is_searching,
             'query_string': q,
+            'pagination_query_params': ('q=%s' % q) if q else ''
         })
 
 


Test patch:
diff --git a/wagtail/wagtailadmin/tests/test_page_chooser.py b/wagtail/wagtailadmin/tests/test_page_chooser.py
--- a/wagtail/wagtailadmin/tests/test_page_chooser.py
+++ b/wagtail/wagtailadmin/tests/test_page_chooser.py
@@ -21,24 +21,27 @@ def setUp(self):
     def get(self, params={}):
         return self.client.get(reverse('wagtailadmin_choose_page'), params)
 
+    def search(self, params={}):
+        return self.client.get(reverse('wagtailadmin_choose_page_search'), params)
+
     def test_simple(self):
         response = self.get()
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, 'wagtailadmin/chooser/browse.html')
 
     def test_search(self):
-        response = self.get({'q': "foobarbaz"})
+        response = self.search({'q': "foobarbaz"})
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, "There is one match")
         self.assertContains(response, "foobarbaz")
 
     def test_search_no_results(self):
-        response = self.get({'q': "quux"})
+        response = self.search({'q': "quux"})
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, "There are 0 matches")
 
     def test_get_invalid(self):
-        response = self.get({'page_type': 'foo.bar'})
+        response = self.search({'page_type': 'foo.bar'})
         self.assertEqual(response.status_code, 404)
 
 
@@ -67,17 +70,6 @@ def test_simple(self):
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, 'wagtailadmin/chooser/browse.html')
 
-    def test_search(self):
-        response = self.get({'q': "foobarbaz"})
-        self.assertEqual(response.status_code, 200)
-        self.assertContains(response, "There is one match")
-        self.assertContains(response, "foobarbaz")
-
-    def test_search_no_results(self):
-        response = self.get({'q': "quux"})
-        self.assertEqual(response.status_code, 200)
-        self.assertContains(response, "There are 0 matches")
-
     def test_get_invalid(self):
         self.assertEqual(self.get_invalid().status_code, 404)