Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/django/core/paginator.py: 35%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

125 statements  

1import collections.abc 

2import inspect 

3import warnings 

4from math import ceil 

5 

6from django.utils.functional import cached_property 

7from django.utils.inspect import method_has_no_args 

8from django.utils.translation import gettext_lazy as _ 

9 

10 

11class UnorderedObjectListWarning(RuntimeWarning): 

12 pass 

13 

14 

15class InvalidPage(Exception): 

16 pass 

17 

18 

19class PageNotAnInteger(InvalidPage): 

20 pass 

21 

22 

23class EmptyPage(InvalidPage): 

24 pass 

25 

26 

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 = _("…") 

31 default_error_messages = { 

32 "invalid_page": _("That page number is not an integer"), 

33 "min_page": _("That page number is less than 1"), 

34 "no_results": _("That page contains no results"), 

35 } 

36 

37 def __init__( 

38 self, 

39 object_list, 

40 per_page, 

41 orphans=0, 

42 allow_empty_first_page=True, 

43 error_messages=None, 

44 ): 

45 self.object_list = object_list 

46 self._check_object_list_is_ordered() 

47 self.per_page = int(per_page) 

48 self.orphans = int(orphans) 

49 self.allow_empty_first_page = allow_empty_first_page 

50 self.error_messages = ( 

51 self.default_error_messages 

52 if error_messages is None 

53 else self.default_error_messages | error_messages 

54 ) 

55 

56 def __iter__(self): 

57 for page_number in self.page_range: 

58 yield self.page(page_number) 

59 

60 def validate_number(self, number): 

61 """Validate the given 1-based page number.""" 

62 try: 

63 if isinstance(number, float) and not number.is_integer(): 

64 raise ValueError 

65 number = int(number) 

66 except (TypeError, ValueError): 

67 raise PageNotAnInteger(self.error_messages["invalid_page"]) 

68 if number < 1: 

69 raise EmptyPage(self.error_messages["min_page"]) 

70 if number > self.num_pages: 

71 raise EmptyPage(self.error_messages["no_results"]) 

72 return number 

73 

74 def get_page(self, number): 

75 """ 

76 Return a valid page, even if the page argument isn't a number or isn't 

77 in range. 

78 """ 

79 try: 

80 number = self.validate_number(number) 

81 except PageNotAnInteger: 

82 number = 1 

83 except EmptyPage: 

84 number = self.num_pages 

85 return self.page(number) 

86 

87 def page(self, number): 

88 """Return a Page object for the given 1-based page number.""" 

89 number = self.validate_number(number) 

90 bottom = (number - 1) * self.per_page 

91 top = bottom + self.per_page 

92 if top + self.orphans >= self.count: 

93 top = self.count 

94 return self._get_page(self.object_list[bottom:top], number, self) 

95 

96 def _get_page(self, *args, **kwargs): 

97 """ 

98 Return an instance of a single page. 

99 

100 This hook can be used by subclasses to use an alternative to the 

101 standard :cls:`Page` object. 

102 """ 

103 return Page(*args, **kwargs) 

104 

105 @cached_property 

106 def count(self): 

107 """Return the total number of objects, across all pages.""" 

108 c = getattr(self.object_list, "count", None) 

109 if callable(c) and not inspect.isbuiltin(c) and method_has_no_args(c): 

110 return c() 

111 return len(self.object_list) 

112 

113 @cached_property 

114 def num_pages(self): 

115 """Return the total number of pages.""" 

116 if self.count == 0 and not self.allow_empty_first_page: 

117 return 0 

118 hits = max(1, self.count - self.orphans) 

119 return ceil(hits / self.per_page) 

120 

121 @property 

122 def page_range(self): 

123 """ 

124 Return a 1-based range of pages for iterating through within 

125 a template for loop. 

126 """ 

127 return range(1, self.num_pages + 1) 

128 

129 def _check_object_list_is_ordered(self): 

130 """ 

131 Warn if self.object_list is unordered (typically a QuerySet). 

132 """ 

133 ordered = getattr(self.object_list, "ordered", None) 

134 if ordered is not None and not ordered: 

135 obj_list_repr = ( 

136 "{} {}".format( 

137 self.object_list.model, self.object_list.__class__.__name__ 

138 ) 

139 if hasattr(self.object_list, "model") 

140 else "{!r}".format(self.object_list) 

141 ) 

142 warnings.warn( 

143 "Pagination may yield inconsistent results with an unordered " 

144 "object_list: {}.".format(obj_list_repr), 

145 UnorderedObjectListWarning, 

146 stacklevel=3, 

147 ) 

148 

149 def get_elided_page_range(self, number=1, *, on_each_side=3, on_ends=2): 

150 """ 

151 Return a 1-based range of pages with some values elided. 

152 

153 If the page range is larger than a given size, the whole range is not 

154 provided and a compact form is returned instead, e.g. for a paginator 

155 with 50 pages, if page 43 were the current page, the output, with the 

156 default arguments, would be: 

157 

158 1, 2, …, 40, 41, 42, 43, 44, 45, 46, …, 49, 50. 

159 """ 

160 number = self.validate_number(number) 

161 

162 if self.num_pages <= (on_each_side + on_ends) * 2: 

163 yield from self.page_range 

164 return 

165 

166 if number > (1 + on_each_side + on_ends) + 1: 

167 yield from range(1, on_ends + 1) 

168 yield self.ELLIPSIS 

169 yield from range(number - on_each_side, number + 1) 

170 else: 

171 yield from range(1, number + 1) 

172 

173 if number < (self.num_pages - on_each_side - on_ends) - 1: 

174 yield from range(number + 1, number + on_each_side + 1) 

175 yield self.ELLIPSIS 

176 yield from range(self.num_pages - on_ends + 1, self.num_pages + 1) 

177 else: 

178 yield from range(number + 1, self.num_pages + 1) 

179 

180 

181class Page(collections.abc.Sequence): 

182 def __init__(self, object_list, number, paginator): 

183 self.object_list = object_list 

184 self.number = number 

185 self.paginator = paginator 

186 

187 def __repr__(self): 

188 return "<Page %s of %s>" % (self.number, self.paginator.num_pages) 

189 

190 def __len__(self): 

191 return len(self.object_list) 

192 

193 def __getitem__(self, index): 

194 if not isinstance(index, (int, slice)): 

195 raise TypeError( 

196 "Page indices must be integers or slices, not %s." 

197 % type(index).__name__ 

198 ) 

199 # The object_list is converted to a list so that if it was a QuerySet 

200 # it won't be a database hit per __getitem__. 

201 if not isinstance(self.object_list, list): 

202 self.object_list = list(self.object_list) 

203 return self.object_list[index] 

204 

205 def has_next(self): 

206 return self.number < self.paginator.num_pages 

207 

208 def has_previous(self): 

209 return self.number > 1 

210 

211 def has_other_pages(self): 

212 return self.has_previous() or self.has_next() 

213 

214 def next_page_number(self): 

215 return self.paginator.validate_number(self.number + 1) 

216 

217 def previous_page_number(self): 

218 return self.paginator.validate_number(self.number - 1) 

219 

220 def start_index(self): 

221 """ 

222 Return the 1-based index of the first object on this page, 

223 relative to total objects in the paginator. 

224 """ 

225 # Special case, return zero if no items. 

226 if self.paginator.count == 0: 

227 return 0 

228 return (self.paginator.per_page * (self.number - 1)) + 1 

229 

230 def end_index(self): 

231 """ 

232 Return the 1-based index of the last object on this page, 

233 relative to total objects found (hits). 

234 """ 

235 # Special case for the last page because there can be orphans. 

236 if self.number == self.paginator.num_pages: 

237 return self.paginator.count 

238 return self.number * self.paginator.per_page