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

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 

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 

38 

39 def __iter__(self): 

40 for page_number in self.page_range: 

41 yield self.page(page_number) 

42 

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 

56 

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) 

69 

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) 

78 

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

80 """ 

81 Return an instance of a single page. 

82 

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) 

87 

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) 

95 

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) 

103 

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) 

111 

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 ) 

131 

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. 

135 

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: 

140 

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

142 """ 

143 number = self.validate_number(number) 

144 

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

146 yield from self.page_range 

147 return 

148 

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) 

155 

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) 

162 

163 

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 

169 

170 def __repr__(self): 

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

172 

173 def __len__(self): 

174 return len(self.object_list) 

175 

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] 

187 

188 def has_next(self): 

189 return self.number < self.paginator.num_pages 

190 

191 def has_previous(self): 

192 return self.number > 1 

193 

194 def has_other_pages(self): 

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

196 

197 def next_page_number(self): 

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

199 

200 def previous_page_number(self): 

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

202 

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 

212 

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