Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/django/core/paginator.py: 35%
123 statements
« prev ^ index » next coverage.py v7.0.5, created at 2023-01-17 06:13 +0000
« prev ^ index » next coverage.py v7.0.5, created at 2023-01-17 06:13 +0000
1import collections.abc
2import inspect
3import warnings
4from math import ceil
6from django.utils.functional import cached_property
7from django.utils.inspect import method_has_no_args
8from django.utils.translation import gettext_lazy as _
11class UnorderedObjectListWarning(RuntimeWarning):
12 pass
15class InvalidPage(Exception):
16 pass
19class PageNotAnInteger(InvalidPage):
20 pass
23class EmptyPage(InvalidPage):
24 pass
27class Paginator:
28 # Translators: String used to replace omitted page numbers in elided page
29 # range generated by paginators, e.g. [1, 2, '…', 5, 6, 7, '…', 9, 10].
30 ELLIPSIS = _("…")
32 def __init__(self, object_list, per_page, orphans=0, allow_empty_first_page=True):
33 self.object_list = object_list
34 self._check_object_list_is_ordered()
35 self.per_page = int(per_page)
36 self.orphans = int(orphans)
37 self.allow_empty_first_page = allow_empty_first_page
39 def __iter__(self):
40 for page_number in self.page_range:
41 yield self.page(page_number)
43 def validate_number(self, number):
44 """Validate the given 1-based page number."""
45 try:
46 if isinstance(number, float) and not number.is_integer():
47 raise ValueError
48 number = int(number)
49 except (TypeError, ValueError):
50 raise PageNotAnInteger(_("That page number is not an integer"))
51 if number < 1:
52 raise EmptyPage(_("That page number is less than 1"))
53 if number > self.num_pages:
54 raise EmptyPage(_("That page contains no results"))
55 return number
57 def get_page(self, number):
58 """
59 Return a valid page, even if the page argument isn't a number or isn't
60 in range.
61 """
62 try:
63 number = self.validate_number(number)
64 except PageNotAnInteger:
65 number = 1
66 except EmptyPage:
67 number = self.num_pages
68 return self.page(number)
70 def page(self, number):
71 """Return a Page object for the given 1-based page number."""
72 number = self.validate_number(number)
73 bottom = (number - 1) * self.per_page
74 top = bottom + self.per_page
75 if top + self.orphans >= self.count:
76 top = self.count
77 return self._get_page(self.object_list[bottom:top], number, self)
79 def _get_page(self, *args, **kwargs):
80 """
81 Return an instance of a single page.
83 This hook can be used by subclasses to use an alternative to the
84 standard :cls:`Page` object.
85 """
86 return Page(*args, **kwargs)
88 @cached_property
89 def count(self):
90 """Return the total number of objects, across all pages."""
91 c = getattr(self.object_list, "count", None)
92 if callable(c) and not inspect.isbuiltin(c) and method_has_no_args(c):
93 return c()
94 return len(self.object_list)
96 @cached_property
97 def num_pages(self):
98 """Return the total number of pages."""
99 if self.count == 0 and not self.allow_empty_first_page:
100 return 0
101 hits = max(1, self.count - self.orphans)
102 return ceil(hits / self.per_page)
104 @property
105 def page_range(self):
106 """
107 Return a 1-based range of pages for iterating through within
108 a template for loop.
109 """
110 return range(1, self.num_pages + 1)
112 def _check_object_list_is_ordered(self):
113 """
114 Warn if self.object_list is unordered (typically a QuerySet).
115 """
116 ordered = getattr(self.object_list, "ordered", None)
117 if ordered is not None and not ordered:
118 obj_list_repr = (
119 "{} {}".format(
120 self.object_list.model, self.object_list.__class__.__name__
121 )
122 if hasattr(self.object_list, "model")
123 else "{!r}".format(self.object_list)
124 )
125 warnings.warn(
126 "Pagination may yield inconsistent results with an unordered "
127 "object_list: {}.".format(obj_list_repr),
128 UnorderedObjectListWarning,
129 stacklevel=3,
130 )
132 def get_elided_page_range(self, number=1, *, on_each_side=3, on_ends=2):
133 """
134 Return a 1-based range of pages with some values elided.
136 If the page range is larger than a given size, the whole range is not
137 provided and a compact form is returned instead, e.g. for a paginator
138 with 50 pages, if page 43 were the current page, the output, with the
139 default arguments, would be:
141 1, 2, …, 40, 41, 42, 43, 44, 45, 46, …, 49, 50.
142 """
143 number = self.validate_number(number)
145 if self.num_pages <= (on_each_side + on_ends) * 2:
146 yield from self.page_range
147 return
149 if number > (1 + on_each_side + on_ends) + 1:
150 yield from range(1, on_ends + 1)
151 yield self.ELLIPSIS
152 yield from range(number - on_each_side, number + 1)
153 else:
154 yield from range(1, number + 1)
156 if number < (self.num_pages - on_each_side - on_ends) - 1:
157 yield from range(number + 1, number + on_each_side + 1)
158 yield self.ELLIPSIS
159 yield from range(self.num_pages - on_ends + 1, self.num_pages + 1)
160 else:
161 yield from range(number + 1, self.num_pages + 1)
164class Page(collections.abc.Sequence):
165 def __init__(self, object_list, number, paginator):
166 self.object_list = object_list
167 self.number = number
168 self.paginator = paginator
170 def __repr__(self):
171 return "<Page %s of %s>" % (self.number, self.paginator.num_pages)
173 def __len__(self):
174 return len(self.object_list)
176 def __getitem__(self, index):
177 if not isinstance(index, (int, slice)):
178 raise TypeError(
179 "Page indices must be integers or slices, not %s."
180 % type(index).__name__
181 )
182 # The object_list is converted to a list so that if it was a QuerySet
183 # it won't be a database hit per __getitem__.
184 if not isinstance(self.object_list, list):
185 self.object_list = list(self.object_list)
186 return self.object_list[index]
188 def has_next(self):
189 return self.number < self.paginator.num_pages
191 def has_previous(self):
192 return self.number > 1
194 def has_other_pages(self):
195 return self.has_previous() or self.has_next()
197 def next_page_number(self):
198 return self.paginator.validate_number(self.number + 1)
200 def previous_page_number(self):
201 return self.paginator.validate_number(self.number - 1)
203 def start_index(self):
204 """
205 Return the 1-based index of the first object on this page,
206 relative to total objects in the paginator.
207 """
208 # Special case, return zero if no items.
209 if self.paginator.count == 0:
210 return 0
211 return (self.paginator.per_page * (self.number - 1)) + 1
213 def end_index(self):
214 """
215 Return the 1-based index of the last object on this page,
216 relative to total objects found (hits).
217 """
218 # Special case for the last page because there can be orphans.
219 if self.number == self.paginator.num_pages:
220 return self.paginator.count
221 return self.number * self.paginator.per_page