Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #6997 -- Corrected `num_pages` calculation when one item is in …

…the object list and `allow_empty_first_page=False`, thanks to framos for the report. Also, made Page's `start_index()` return 0 if there are no items in the object list (previously it was returning 1, but now it is consistent with `end_index()`). As an added bonus, I threw in quite a few pagination tests.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@8129 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 96cf3656c48f6c42714a70b4546bc42f7b904185 1 parent 19c7db0
Gary Wilson Jr. authored July 28, 2008
14  django/core/paginator.py
... ...
@@ -1,3 +1,5 @@
  1
+from math import ceil
  2
+
1 3
 class InvalidPage(Exception):
2 4
     pass
3 5
 
@@ -55,13 +57,11 @@ def _get_count(self):
55 57
     def _get_num_pages(self):
56 58
         "Returns the total number of pages."
57 59
         if self._num_pages is None:
58  
-            hits = self.count - 1 - self.orphans
59  
-            if hits < 1:
60  
-                hits = 0
61  
-            if hits == 0 and not self.allow_empty_first_page:
  60
+            if self.count == 0 and not self.allow_empty_first_page:
62 61
                 self._num_pages = 0
63 62
             else:
64  
-                self._num_pages = hits // self.per_page + 1
  63
+                hits = max(1, self.count - self.orphans)
  64
+                self._num_pages = int(ceil(hits / float(self.per_page)))
65 65
         return self._num_pages
66 66
     num_pages = property(_get_num_pages)
67 67
 
@@ -104,6 +104,9 @@ def start_index(self):
104 104
         Returns the 1-based index of the first object on this page,
105 105
         relative to total objects in the paginator.
106 106
         """
  107
+        # Special case, return zero if no items.
  108
+        if self.paginator.count == 0:
  109
+            return 0
107 110
         return (self.paginator.per_page * (self.number - 1)) + 1
108 111
 
109 112
     def end_index(self):
@@ -111,6 +114,7 @@ def end_index(self):
111 114
         Returns the 1-based index of the last object on this page,
112 115
         relative to total objects found (hits).
113 116
         """
  117
+        # Special case for the last page because there can be orphans.
114 118
         if self.number == self.paginator.num_pages:
115 119
             return self.paginator.count
116 120
         return self.number * self.paginator.per_page
0  tests/regressiontests/pagination_regress/__init__.py
No changes.
1  tests/regressiontests/pagination_regress/models.py
... ...
@@ -0,0 +1 @@
  1
+# Models file for tests to run.
157  tests/regressiontests/pagination_regress/tests.py
... ...
@@ -0,0 +1,157 @@
  1
+from unittest import TestCase
  2
+
  3
+from django.core.paginator import Paginator, EmptyPage
  4
+
  5
+class PaginatorTests(TestCase):
  6
+    """
  7
+    Tests for the Paginator and Page classes.
  8
+    """
  9
+
  10
+    def check_paginator(self, params, output):
  11
+        """
  12
+        Helper method that instantiates a Paginator object from the passed
  13
+        params and then checks that it's attributes match the passed output.
  14
+        """
  15
+        count, num_pages, page_range = output
  16
+        paginator = Paginator(*params)
  17
+        self.check_attribute('count', paginator, count, params)
  18
+        self.check_attribute('num_pages', paginator, num_pages, params)
  19
+        self.check_attribute('page_range', paginator, page_range, params)
  20
+
  21
+    def check_attribute(self, name, paginator, expected, params):
  22
+        """
  23
+        Helper method to check a single attribute and gives a nice error
  24
+        message upon test failure.
  25
+        """
  26
+        got = getattr(paginator, name)
  27
+        self.assertEqual(expected, got,
  28
+            "For '%s', expected %s but got %s.  Paginator parameters were: %s"
  29
+            % (name, expected, got, params))
  30
+
  31
+    def test_paginator(self):
  32
+        """
  33
+        Tests the paginator attributes using varying inputs.
  34
+        """
  35
+        nine = [1, 2, 3, 4, 5, 6, 7, 8, 9]
  36
+        ten = nine + [10]
  37
+        eleven = ten + [11]
  38
+        tests = (
  39
+            # Each item is two tuples:
  40
+            #     First tuple is Paginator parameters - object_list, per_page,
  41
+            #         orphans, and allow_empty_first_page.
  42
+            #     Second tuple is resulting Paginator attributes - count,
  43
+            #         num_pages, and page_range.
  44
+            # Ten items, varying orphans, no empty first page.
  45
+            ((ten, 4, 0, False), (10, 3, [1, 2, 3])),
  46
+            ((ten, 4, 1, False), (10, 3, [1, 2, 3])),
  47
+            ((ten, 4, 2, False), (10, 2, [1, 2])),
  48
+            ((ten, 4, 5, False), (10, 2, [1, 2])),
  49
+            ((ten, 4, 6, False), (10, 1, [1])),
  50
+            # Ten items, varying orphans, allow empty first page.
  51
+            ((ten, 4, 0, True), (10, 3, [1, 2, 3])),
  52
+            ((ten, 4, 1, True), (10, 3, [1, 2, 3])),
  53
+            ((ten, 4, 2, True), (10, 2, [1, 2])),
  54
+            ((ten, 4, 5, True), (10, 2, [1, 2])),
  55
+            ((ten, 4, 6, True), (10, 1, [1])),
  56
+            # One item, varying orphans, no empty first page.
  57
+            (([1], 4, 0, False), (1, 1, [1])),
  58
+            (([1], 4, 1, False), (1, 1, [1])),
  59
+            (([1], 4, 2, False), (1, 1, [1])),
  60
+            # One item, varying orphans, allow empty first page.
  61
+            (([1], 4, 0, True), (1, 1, [1])),
  62
+            (([1], 4, 1, True), (1, 1, [1])),
  63
+            (([1], 4, 2, True), (1, 1, [1])),
  64
+            # Zero items, varying orphans, no empty first page.
  65
+            (([], 4, 0, False), (0, 0, [])),
  66
+            (([], 4, 1, False), (0, 0, [])),
  67
+            (([], 4, 2, False), (0, 0, [])),
  68
+            # Zero items, varying orphans, allow empty first page.
  69
+            (([], 4, 0, True), (0, 1, [1])),
  70
+            (([], 4, 1, True), (0, 1, [1])),
  71
+            (([], 4, 2, True), (0, 1, [1])),
  72
+            # Number if items one less than per_page.
  73
+            (([], 1, 0, True), (0, 1, [1])),
  74
+            (([], 1, 0, False), (0, 0, [])),
  75
+            (([1], 2, 0, True), (1, 1, [1])),
  76
+            ((nine, 10, 0, True), (9, 1, [1])),
  77
+            # Number if items equal to per_page.
  78
+            (([1], 1, 0, True), (1, 1, [1])),
  79
+            (([1, 2], 2, 0, True), (2, 1, [1])),
  80
+            ((ten, 10, 0, True), (10, 1, [1])),
  81
+            # Number if items one more than per_page.
  82
+            (([1, 2], 1, 0, True), (2, 2, [1, 2])),
  83
+            (([1, 2, 3], 2, 0, True), (3, 2, [1, 2])),
  84
+            ((eleven, 10, 0, True), (11, 2, [1, 2])),
  85
+            # Number if items one more than per_page with one orphan.
  86
+            (([1, 2], 1, 1, True), (2, 1, [1])),
  87
+            (([1, 2, 3], 2, 1, True), (3, 1, [1])),
  88
+            ((eleven, 10, 1, True), (11, 1, [1])),
  89
+        )
  90
+        for params, output in tests:
  91
+            self.check_paginator(params, output)
  92
+
  93
+    def check_indexes(self, params, page_num, indexes):
  94
+        """
  95
+        Helper method that instantiates a Paginator object from the passed
  96
+        params and then checks that the start and end indexes of for the passed
  97
+        page_num match those given as a 2-tuple in indexes.
  98
+        """
  99
+        paginator = Paginator(*params)
  100
+        if page_num == 'first':
  101
+            page_num = 1
  102
+        elif page_num == 'last':
  103
+            page_num = paginator.num_pages
  104
+        page = paginator.page(page_num)
  105
+        start, end = indexes
  106
+        msg = ("For %s of page %s, expected %s but got %s."
  107
+               " Paginator parameters were: %s")
  108
+        self.assertEqual(start, page.start_index(),
  109
+            msg % ('start index', page_num, start, page.start_index(), params))
  110
+        self.assertEqual(end, page.end_index(),
  111
+            msg % ('end index', page_num, end, page.end_index(), params))
  112
+
  113
+    def test_page_indexes(self):
  114
+        """
  115
+        Tests that paginator pages have the correct start and end indexes.
  116
+        """
  117
+        ten = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  118
+        tests = (
  119
+            # Each item is three tuples:
  120
+            #     First tuple is Paginator parameters - object_list, per_page,
  121
+            #         orphans, and allow_empty_first_page.
  122
+            #     Second tuple is the start and end indexes of the first page.
  123
+            #     Third tuple is the start and end indexes of the last page.
  124
+            # Ten items, varying per_page, no orphans.
  125
+            ((ten, 1, 0, True), (1, 1), (10, 10)),
  126
+            ((ten, 2, 0, True), (1, 2), (9, 10)),
  127
+            ((ten, 3, 0, True), (1, 3), (10, 10)),
  128
+            ((ten, 5, 0, True), (1, 5), (6, 10)),
  129
+            # Ten items, varying per_page, with orphans.
  130
+            ((ten, 1, 1, True), (1, 1), (9, 10)),
  131
+            ((ten, 1, 2, True), (1, 1), (8, 10)),
  132
+            ((ten, 3, 1, True), (1, 3), (7, 10)),
  133
+            ((ten, 3, 2, True), (1, 3), (7, 10)),
  134
+            ((ten, 3, 4, True), (1, 3), (4, 10)),
  135
+            ((ten, 5, 1, True), (1, 5), (6, 10)),
  136
+            ((ten, 5, 2, True), (1, 5), (6, 10)),
  137
+            ((ten, 5, 5, True), (1, 10), (1, 10)),
  138
+            # One item, varying orphans, no empty first page.
  139
+            (([1], 4, 0, False), (1, 1), (1, 1)),
  140
+            (([1], 4, 1, False), (1, 1), (1, 1)),
  141
+            (([1], 4, 2, False), (1, 1), (1, 1)),
  142
+            # One item, varying orphans, allow empty first page.
  143
+            (([1], 4, 0, True), (1, 1), (1, 1)),
  144
+            (([1], 4, 1, True), (1, 1), (1, 1)),
  145
+            (([1], 4, 2, True), (1, 1), (1, 1)),
  146
+            # Zero items, varying orphans, allow empty first page.
  147
+            (([], 4, 0, True), (0, 0), (0, 0)),
  148
+            (([], 4, 1, True), (0, 0), (0, 0)),
  149
+            (([], 4, 2, True), (0, 0), (0, 0)),
  150
+        )
  151
+        for params, first, last in tests:
  152
+            self.check_indexes(params, 'first', first)
  153
+            self.check_indexes(params, 'last', last)
  154
+        # When no items and no empty first page, we should get EmptyPage error.
  155
+        self.assertRaises(EmptyPage, self.check_indexes, ([], 4, 0, False), 1, None)
  156
+        self.assertRaises(EmptyPage, self.check_indexes, ([], 4, 1, False), 1, None)
  157
+        self.assertRaises(EmptyPage, self.check_indexes, ([], 4, 2, False), 1, None)

0 notes on commit 96cf365

Please sign in to comment.
Something went wrong with that request. Please try again.