summaryrefslogtreecommitdiff
path: root/django/core/paginator.py
blob: b50ca826c4172f144af473a76fb724ce28ff488d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
class InvalidPage(Exception):
    pass

class ObjectPaginator(object):
    """
    This class makes pagination easy. Feed it a QuerySet or list, plus the number
    of objects you want on each page. Then read the hits and pages properties to
    see how many pages it involves. Call get_page with a page number (starting
    at 0) to get back a list of objects for that page.

    Finally, check if a page number has a next/prev page using
    has_next_page(page_number) and has_previous_page(page_number).
    
    Use orphans to avoid small final pages. For example:
    13 records, num_per_page=10, orphans=2 --> pages==2, len(self.get_page(0))==10
    12 records, num_per_page=10, orphans=2 --> pages==1, len(self.get_page(0))==12
    """
    def __init__(self, query_set, num_per_page, orphans=0):
        self.query_set = query_set
        self.num_per_page = num_per_page
        self.orphans = orphans
        self._hits = self._pages = None
        self._page_range = None

    def validate_page_number(self, page_number):
        try:
            page_number = int(page_number)
        except ValueError:
            raise InvalidPage
        if page_number < 0 or page_number > self.pages - 1:
            raise InvalidPage
        return page_number

    def get_page(self, page_number):
        page_number = self.validate_page_number(page_number)
        bottom = page_number * self.num_per_page
        top = bottom + self.num_per_page
        if top + self.orphans >= self.hits:
            top = self.hits
        return self.query_set[bottom:top]

    def has_next_page(self, page_number):
        "Does page $page_number have a 'next' page?"
        return page_number < self.pages - 1

    def has_previous_page(self, page_number):
        return page_number > 0

    def first_on_page(self, page_number):
        """
        Returns the 1-based index of the first object on the given page,
        relative to total objects found (hits).
        """
        page_number = self.validate_page_number(page_number)
        return (self.num_per_page * page_number) + 1

    def last_on_page(self, page_number):
        """
        Returns the 1-based index of the last object on the given page,
        relative to total objects found (hits).
        """
        page_number = self.validate_page_number(page_number)
        page_number += 1   # 1-base
        if page_number == self.pages:
            return self.hits
        return page_number * self.num_per_page

    def _get_hits(self):
        if self._hits is None:
            # Try .count() or fall back to len().
            try:
                self._hits = int(self.query_set.count())
            except (AttributeError, TypeError, ValueError):
                # AttributeError if query_set has no object count.
                # TypeError if query_set.count() required arguments.
                # ValueError if int() fails.
                self._hits = len(self.query_set)
        return self._hits

    def _get_pages(self):
        if self._pages is None:
            hits = (self.hits - 1 - self.orphans)
            if hits < 1:
                hits = 0
            self._pages = hits // self.num_per_page + 1
        return self._pages
    
    def _get_page_range(self):
        """
        Returns a 1-based range of pages for iterating through within 
        a template for loop.
        """
        if self._page_range is None:
            self._page_range = range(1, self._pages + 1)
        return self._page_range

    hits = property(_get_hits)
    pages = property(_get_pages)
    page_range = property(_get_page_range)