import collections import inspect from math import ceil from django.core.paginator import EmptyPage, InvalidPage from django.http import Http404 from django.utils.functional import cached_property from django.utils.inspect import method_has_no_args class InfinitePage(collections.abc.Sequence): def __init__( self, object_list, number, unfiltered_queryset, page_size, pad_pages, paginator ): self.object_list = list(object_list) self.number = number self.unfiltered_queryset = unfiltered_queryset self.page_size = page_size self.pad_pages = pad_pages self.num_pages = 1e3000 self.paginator = paginator def __repr__(self): return "" % self.number def __len__(self): return len(self.object_list) def __getitem__(self, index): return self.object_list[index] @cached_property def _after_up_to_pad(self): first_after = self.number * self.page_size padding_length = self.pad_pages * self.page_size queryset = self.unfiltered_queryset[ first_after : first_after + padding_length + 1 ] c = getattr(queryset, "count", None) if callable(c) and not inspect.isbuiltin(c) and method_has_no_args(c): return c() return len(queryset) def has_next(self): return self._after_up_to_pad > 0 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): if not self.has_next(): raise EmptyPage() return self.number + 1 def previous_page_number(self): if self.number <= 1: raise EmptyPage() return self.number - 1 def start_index(self): return (self.page_size * (self.number - 1)) + 1 def end_index(self): return self.start_index() + len(self.object_list) @cached_property def main_range(self): start = max(1, self.number - self.pad_pages) end = self.number + min( int(ceil(self._after_up_to_pad / self.page_size)), self.pad_pages ) return range(start, end + 1) @cached_property def leading_range(self): return range(1, min(3, self.main_range[0])) @cached_property def has_trailing(self): return self._after_up_to_pad > self.pad_pages * self.page_size @cached_property def page_range(self): result = list(self.leading_range) main_range = self.main_range # Add ... element if there is space in between. if result and result[-1] + 1 < self.main_range[0]: result.append(False) result += list(main_range) # Add ... element if there are elements after main_range. if self.has_trailing: result.append(False) return result class DummyPaginator: is_infinite = True def __init__(self, per_page): self.per_page = per_page def infinite_paginate(queryset, page, page_size, pad_pages, paginator=None): if page < 1: raise EmptyPage() sliced = queryset[(page - 1) * page_size : page * page_size] if page > 1 and not sliced: raise EmptyPage() return InfinitePage(sliced, page, queryset, page_size, pad_pages, paginator) class InfinitePaginationMixin: pad_pages = 4 @property def use_infinite_pagination(self): return True def paginate_queryset(self, queryset, page_size): if not self.use_infinite_pagination: paginator, page, object_list, has_other = super().paginate_queryset( queryset, page_size ) paginator.is_infinite = False return paginator, page, object_list, has_other page_kwarg = self.page_kwarg page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1 try: page_number = int(page) except ValueError: raise Http404("Page cannot be converted to an int.") try: paginator = DummyPaginator(page_size) page = infinite_paginate( queryset, page_number, page_size, self.pad_pages, paginator ) return paginator, page, page.object_list, page.has_other_pages() except InvalidPage as e: raise Http404( "Invalid page (%(page_number)s): %(message)s" % { "page_number": page_number, "message": str(e), } )