commit 263bbe728adafa2b59671b537ce92a1ec5bee06b
Author: Malcolm Box <Malcolm Box boxm@livetalkback.com>
Date:   Thu May 26 14:31:39 2011 +0100

    Patches to Django to allow admin screens to use a Paginator that doesn't use
    count() because that's expensive

diff --git ./django/contrib/admin/options.py ./django/contrib/admin/options.py
index 43c5503..25d32eb 100644
--- ./django/contrib/admin/options.py
+++ ./django/contrib/admin/options.py
@@ -1151,13 +1151,9 @@ class ModelAdmin(BaseModelAdmin):
         else:
             action_form = None
 
-        selection_note_all = ungettext('%(total_count)s selected',
-            'All %(total_count)s selected', cl.result_count)
-
         context = {
             'module_name': force_unicode(opts.verbose_name_plural),
             'selection_note': _('0 of %(cnt)s selected') % {'cnt': len(cl.result_list)},
-            'selection_note_all': selection_note_all % {'total_count': cl.result_count},
             'title': cl.title,
             'is_popup': cl.is_popup,
             'cl': cl,
@@ -1170,6 +1166,12 @@ class ModelAdmin(BaseModelAdmin):
             'actions_on_bottom': self.actions_on_bottom,
             'actions_selection_counter': self.actions_selection_counter,
         }
+
+        if hasattr(cl, 'result_count'):
+            selection_note_all = ungettext('%(total_count)s selected',
+                'All %(total_count)s selected', cl.result_count)
+            context['selection_note_all'] = selection_note_all % {'total_count': cl.result_count}
+
         context.update(extra_context or {})
         context_instance = template.RequestContext(request, current_app=self.admin_site.name)
         return render_to_response(self.change_list_template or [
diff --git ./django/contrib/admin/templates/admin/pagination.html ./django/contrib/admin/templates/admin/pagination.html
index 3588132..4f28356 100644
--- ./django/contrib/admin/templates/admin/pagination.html
+++ ./django/contrib/admin/templates/admin/pagination.html
@@ -6,7 +6,9 @@
     {% paginator_number cl i %}
 {% endfor %}
 {% endif %}
+{% if cl.result_count != None %}
 {{ cl.result_count }} {% ifequal cl.result_count 1 %}{{ cl.opts.verbose_name }}{% else %}{{ cl.opts.verbose_name_plural }}{% endifequal %}
+{% endif %}
 {% if show_all_url %}&nbsp;&nbsp;<a href="{{ show_all_url }}" class="showall">{% trans 'Show all' %}</a>{% endif %}
 {% if cl.formset and cl.result_count %}<input type="submit" name="_save" class="default" value="{% trans 'Save' %}"/>{% endif %}
 </p>
diff --git ./django/contrib/admin/templatetags/admin_list.py ./django/contrib/admin/templatetags/admin_list.py
index fdf082b..96a3c64 100644
--- ./django/contrib/admin/templatetags/admin_list.py
+++ ./django/contrib/admin/templatetags/admin_list.py
@@ -18,6 +18,7 @@ from django.template import Library
 register = Library()
 
 DOT = '.'
+NEXT = 'N'
 
 def paginator_number(cl,i):
     """
@@ -25,16 +26,25 @@ def paginator_number(cl,i):
     """
     if i == DOT:
         return u'... '
+    elif i == NEXT:
+        return mark_safe(u'<a href="%s"> Next</a> ' % (escape(cl.get_query_string({PAGE_VAR: cl.page_num+1}))))
     elif i == cl.page_num:
         return mark_safe(u'<span class="this-page">%d</span> ' % (i+1))
     else:
-        return mark_safe(u'<a href="%s"%s>%d</a> ' % (escape(cl.get_query_string({PAGE_VAR: i})), (i == cl.paginator.num_pages-1 and ' class="end"' or ''), i+1))
+        if hasattr(cl.paginator, 'num_pages'):
+            return mark_safe(u'<a href="%s"%s>%d</a> ' % (escape(cl.get_query_string({PAGE_VAR: i})), (i == cl.paginator.num_pages-1 and ' class="end"' or ''), i+1))
+        else:
+            return mark_safe(u'<a href="%s">%d</a> ' % (escape(cl.get_query_string({PAGE_VAR: i})), i+1))
+            
 paginator_number = register.simple_tag(paginator_number)
 
 def pagination(cl):
     """
     Generates the series of links to the pages in a paginated list.
+
+    If the paginator doesn't support count() then provide next links
     """
+    # page_num is 0 based here
     paginator, page_num = cl.paginator, cl.page_num
 
     pagination_required = (not cl.show_all or not cl.can_show_all) and cl.multi_page
@@ -43,28 +53,41 @@ def pagination(cl):
     else:
         ON_EACH_SIDE = 3
         ON_ENDS = 2
-
-        # If there are 10 or fewer pages, display links to every page.
-        # Otherwise, do some fancy
-        if paginator.num_pages <= 10:
-            page_range = range(paginator.num_pages)
+        if hasattr(paginator, 'num_pages'):
+            # If there are 10 or fewer pages, display links to every page.
+            # Otherwise, do some fancy
+            if paginator.num_pages <= 10:
+                page_range = range(paginator.num_pages)
+            else:
+                # Insert "smart" pagination links, so that there are always ON_ENDS
+                # links at either end of the list of pages, and there are always
+                # ON_EACH_SIDE links at either end of the "current page" link.
+                page_range = []
+                if page_num > (ON_EACH_SIDE + ON_ENDS):
+                    page_range.extend(range(0, ON_EACH_SIDE - 1))
+                    page_range.append(DOT)
+                    page_range.extend(range(page_num - ON_EACH_SIDE, page_num + 1))
+                else:
+                    page_range.extend(range(0, page_num + 1))
+                if page_num < (paginator.num_pages - ON_EACH_SIDE - ON_ENDS - 1):
+                    page_range.extend(range(page_num + 1, page_num + ON_EACH_SIDE + 1))
+                    page_range.append(DOT)
+                    page_range.extend(range(paginator.num_pages - ON_ENDS, paginator.num_pages))
+                else:
+                    page_range.extend(range(page_num + 1, paginator.num_pages))
         else:
-            # Insert "smart" pagination links, so that there are always ON_ENDS
-            # links at either end of the list of pages, and there are always
-            # ON_EACH_SIDE links at either end of the "current page" link.
+            # This paginator doesn't support counts, so provide next link
             page_range = []
             if page_num > (ON_EACH_SIDE + ON_ENDS):
-                page_range.extend(range(0, ON_EACH_SIDE - 1))
-                page_range.append(DOT)
-                page_range.extend(range(page_num - ON_EACH_SIDE, page_num + 1))
-            else:
-                page_range.extend(range(0, page_num + 1))
-            if page_num < (paginator.num_pages - ON_EACH_SIDE - ON_ENDS - 1):
-                page_range.extend(range(page_num + 1, page_num + ON_EACH_SIDE + 1))
+                page_range.extend(range(0, ON_ENDS))
                 page_range.append(DOT)
-                page_range.extend(range(paginator.num_pages - ON_ENDS, paginator.num_pages))
+                page_range.extend(range(page_num - ON_EACH_SIDE, page_num+1))
             else:
-                page_range.extend(range(page_num + 1, paginator.num_pages))
+                page_range.extend(range(0, page_num+1))
+            # Check if we can get the next page, and put the next link in if so
+            page = paginator.page(page_num+1)
+            if page.has_next():
+                page_range.extend(NEXT)
 
     need_show_all_link = cl.can_show_all and not cl.show_all and cl.multi_page
     return {
@@ -309,11 +332,18 @@ def search_form(cl):
     """
     Displays a search form for searching the list.
     """
-    return {
-        'cl': cl,
-        'show_result_count': cl.result_count != cl.full_result_count,
-        'search_var': SEARCH_VAR
-    }
+    if hasattr(cl, 'result_count'):
+        return {
+            'cl': cl,
+            'show_result_count': cl.result_count != cl.full_result_count,
+            'search_var': SEARCH_VAR
+        }
+    else:
+        return {
+            'cl': cl,
+            'search_var': SEARCH_VAR
+        }
+        
 search_form = register.inclusion_tag('admin/search_form.html')(search_form)
 
 def admin_list_filter(cl, spec):
diff --git ./django/contrib/admin/views/main.py ./django/contrib/admin/views/main.py
index 170d168..ff30f7a 100644
--- ./django/contrib/admin/views/main.py
+++ ./django/contrib/admin/views/main.py
@@ -98,19 +98,26 @@ class ChangeList(object):
     def get_results(self, request):
         paginator = self.model_admin.get_paginator(request, self.query_set, self.list_per_page)
         # Get the number of objects, with admin filters applied.
-        result_count = paginator.count
+        if hasattr(paginator, 'count'):
+            result_count = paginator.count
 
-        # Get the total number of objects, with no admin filters applied.
-        # Perform a slight optimization: Check to see whether any filters were
-        # given. If not, use paginator.hits to calculate the number of objects,
-        # because we've already done paginator.hits and the value is cached.
-        if not self.query_set.query.where:
-            full_result_count = result_count
-        else:
-            full_result_count = self.root_query_set.count()
+            # Get the total number of objects, with no admin filters applied.
+            # Perform a slight optimization: Check to see whether any filters were
+            # given. If not, use paginator.hits to calculate the number of objects,
+            # because we've already done paginator.hits and the value is cached.
+            if not self.query_set.query.where:
+                full_result_count = result_count
+            else:
+                full_result_count = self.root_query_set.count()
 
-        can_show_all = result_count <= MAX_SHOW_ALL_ALLOWED
-        multi_page = result_count > self.list_per_page
+            can_show_all = result_count <= MAX_SHOW_ALL_ALLOWED
+            multi_page = result_count > self.list_per_page
+            self.result_count = result_count
+            self.full_result_count = full_result_count
+        else:
+            can_show_all = False
+            multi_page = True
+            self.full_result_count = True
 
         # Get the list of objects to display on this page.
         if (self.show_all and can_show_all) or not multi_page:
@@ -120,9 +127,7 @@ class ChangeList(object):
                 result_list = paginator.page(self.page_num+1).object_list
             except InvalidPage:
                 raise IncorrectLookupParameters
-
-        self.result_count = result_count
-        self.full_result_count = full_result_count
+            
         self.result_list = result_list
         self.can_show_all = can_show_all
         self.multi_page = multi_page
diff --git ./django/core/paginator.py ./django/core/paginator.py
index 495cdf2..4232686 100644
--- ./django/core/paginator.py
+++ ./django/core/paginator.py
@@ -9,6 +9,7 @@ class PageNotAnInteger(InvalidPage):
 class EmptyPage(InvalidPage):
     pass
 
+    
 class Paginator(object):
     def __init__(self, object_list, per_page, orphans=0, allow_empty_first_page=True):
         self.object_list = object_list
@@ -72,13 +73,13 @@ class Paginator(object):
         """
         return range(1, self.num_pages + 1)
     page_range = property(_get_page_range)
-
+ 
 QuerySetPaginator = Paginator # For backwards-compatibility.
 
 class Page(object):
     def __init__(self, object_list, number, paginator):
         self.object_list = object_list
-        self.number = number
+        self.number = number  # 1-based
         self.paginator = paginator
 
     def __repr__(self):
@@ -118,3 +119,59 @@ class Page(object):
         if self.number == self.paginator.num_pages:
             return self.paginator.count
         return self.number * self.paginator.per_page
+
+
+class UncountedPaginator(object):
+    """Pagination for collections that don't support count()/len()"""
+
+    def __init__(self, object_list, per_page):
+        self.object_list = object_list
+        self.per_page = per_page
+
+    def validate_number(self, number):
+        "Validates the given 1-based page number."
+        try:
+            number = int(number)
+        except ValueError:
+            raise PageNotAnInteger('That page number is not an integer')
+        if number < 1:
+            raise EmptyPage('That page number is less than 1')
+        return number
+
+    def page(self, number):
+        "Returns a UncountedPage object for the given 1-based page number."
+        number = self.validate_number(number)
+        bottom = (number - 1) * self.per_page
+        top = bottom + self.per_page
+        return UncountedPage(self.object_list[bottom:top], number, self)
+
+
+class UncountedPage(Page):
+    """ Page for collections that don't support count()"""
+    
+    def __repr__(self):
+        return '<Page %s>' % (self.number)
+
+    def has_next(self):
+        try:
+            next = self.paginator.object_list[
+                self.number*self.paginator.per_page]
+        except IndexError:
+            return False
+        
+        return True
+
+    def start_index(self):
+        """
+        Returns the 1-based index of the first object on this page,
+        relative to total objects in the paginator.
+        """
+        # Special case, return zero if no items.
+        return (self.paginator.per_page * (self.number - 1)) + 1
+
+    def end_index(self):
+        """
+        Returns the 1-based index of the last object on this page,
+        relative to total objects found (hits).
+        """
+        return self.number * self.paginator.per_page
