summaryrefslogtreecommitdiff
path: root/pelican/paginator.py
blob: 4231e67bec86710f2faf3ba5ea574b9e76ee59fd (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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import functools
import logging
import os
from collections import namedtuple
from math import ceil

logger = logging.getLogger(__name__)
PaginationRule = namedtuple(
    'PaginationRule',
    'min_page URL SAVE_AS',
)


class Paginator:
    def __init__(self, name, url, object_list, settings, per_page=None):
        self.name = name
        self.url = url
        self.object_list = object_list
        self.settings = settings
        if per_page:
            self.per_page = per_page
            self.orphans = settings['DEFAULT_ORPHANS']
        else:
            self.per_page = len(object_list)
            self.orphans = 0

        self._num_pages = self._count = None

    def page(self, number):
        "Returns a Page object for the given 1-based page number."
        bottom = (number - 1) * self.per_page
        top = bottom + self.per_page
        if top + self.orphans >= self.count:
            top = self.count
        return Page(self.name, self.url, self.object_list[bottom:top], number,
                    self, self.settings)

    def _get_count(self):
        "Returns the total number of objects, across all pages."
        if self._count is None:
            self._count = len(self.object_list)
        return self._count
    count = property(_get_count)

    def _get_num_pages(self):
        "Returns the total number of pages."
        if self._num_pages is None:
            hits = max(1, self.count - self.orphans)
            self._num_pages = int(ceil(hits / (float(self.per_page) or 1)))
        return self._num_pages
    num_pages = property(_get_num_pages)

    def _get_page_range(self):
        """
        Returns a 1-based range of pages for iterating through within
        a template for loop.
        """
        return list(range(1, self.num_pages + 1))
    page_range = property(_get_page_range)


class Page:
    def __init__(self, name, url, object_list, number, paginator, settings):
        self.full_name = name
        self.name, self.extension = os.path.splitext(name)
        dn, fn = os.path.split(name)
        self.base_name = dn if fn in ('index.htm', 'index.html') else self.name
        self.base_url = url
        self.object_list = object_list
        self.number = number
        self.paginator = paginator
        self.settings = settings

    def __repr__(self):
        return '<Page {} of {}>'.format(self.number, self.paginator.num_pages)

    def has_next(self):
        return self.number < self.paginator.num_pages

    def has_previous(self):
        return self.number > 1

    def has_other_pages(self):
        return self.has_previous() or self.has_next()

    def next_page_number(self):
        return self.number + 1

    def previous_page_number(self):
        return self.number - 1

    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.
        if self.paginator.count == 0:
            return 0
        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).
        """
        # Special case for the last page because there can be orphans.
        if self.number == self.paginator.num_pages:
            return self.paginator.count
        return self.number * self.paginator.per_page

    def _from_settings(self, key):
        """Returns URL information as defined in settings. Similar to
        URLWrapper._from_settings, but specialized to deal with pagination
        logic."""

        rule = None

        # find the last matching pagination rule
        for p in self.settings['PAGINATION_PATTERNS']:
            if p.min_page == -1:
                if not self.has_next():
                    rule = p
                    break
            else:
                if p.min_page <= self.number:
                    rule = p

        if not rule:
            return ''

        prop_value = getattr(rule, key)

        if not isinstance(prop_value, str):
            logger.warning('%s is set to %s', key, prop_value)
            return prop_value

        # URL or SAVE_AS is a string, format it with a controlled context
        context = {
            'save_as': self.full_name,
            'url': self.base_url,
            'name': self.name,
            'base_name': self.base_name,
            'extension': self.extension,
            'number': self.number,
        }

        ret = prop_value.format(**context)
        # Remove a single leading slash, if any. This is done for backwards
        # compatibility reasons. If a leading slash is needed (for URLs
        # relative to server root or absolute URLs without the scheme such as
        # //blog.my.site/), it can be worked around by prefixing the pagination
        # pattern by an additional slash (which then gets removed, preserving
        # the other slashes). This also means the following code *can't* be
        # changed to lstrip() because that would remove all leading slashes and
        # thus make the workaround impossible. See
        # test_custom_pagination_pattern() for a verification of this.
        if ret.startswith('/'):
            ret = ret[1:]
        return ret

    url = property(functools.partial(_from_settings, key='URL'))
    save_as = property(functools.partial(_from_settings, key='SAVE_AS'))