Tweaks, and add pagination controls for offset/limit.

This commit is contained in:
Tom Christie 2015-01-15 16:52:07 +00:00
parent 313aa727e3
commit d76e83dd78
5 changed files with 119 additions and 41 deletions

View File

@ -150,21 +150,21 @@ class GenericAPIView(views.APIView):
return queryset return queryset
@property @property
def pager(self): def paginator(self):
if not hasattr(self, '_pager'): if not hasattr(self, '_paginator'):
if self.pagination_class is None: if self.pagination_class is None:
self._pager = None self._paginator = None
else: else:
self._pager = self.pagination_class() self._paginator = self.pagination_class()
return self._pager return self._paginator
def paginate_queryset(self, queryset): def paginate_queryset(self, queryset):
if self.pager is None: if self.paginator is None:
return queryset return queryset
return self.pager.paginate_queryset(queryset, self.request, view=self) return self.paginator.paginate_queryset(queryset, self.request, view=self)
def get_paginated_response(self, data): def get_paginated_response(self, data):
return self.pager.get_paginated_response(data) return self.paginator.get_paginated_response(data)
# Concrete view classes that provide method handlers # Concrete view classes that provide method handlers

View File

@ -29,6 +29,15 @@ def _strict_positive_int(integer_string, cutoff=None):
return ret return ret
def _divide_with_ceil(a, b):
"""
Returns 'a' divded by 'b', with any remainder rounded up.
"""
if a % b:
return (a / b) + 1
return a / b
def _get_count(queryset): def _get_count(queryset):
""" """
Determine an object count, supporting either querysets or regular lists. Determine an object count, supporting either querysets or regular lists.
@ -48,14 +57,21 @@ def _get_displayed_page_numbers(current, final):
current=14, final=16 -> [1, None, 13, 14, 15, 16] current=14, final=16 -> [1, None, 13, 14, 15, 16]
This implementation gives one page to each side of the cursor, This implementation gives one page to each side of the cursor,
for an implementation which gives two pages to each side of the cursor, or two pages to the side when the cursor is at the edge, then
which is a copy of how GitHub treat pagination in their issue lists, see: ensures that any breaks between non-continous page numbers never
remove only a single page.
For an alernativative implementation which gives two pages to each side of
the cursor, eg. as in GitHub issue list pagination, see:
https://gist.github.com/tomchristie/321140cebb1c4a558b15 https://gist.github.com/tomchristie/321140cebb1c4a558b15
""" """
assert current >= 1 assert current >= 1
assert final >= current assert final >= current
if final <= 5:
return range(1, final + 1)
# We always include the first two pages, last two pages, and # We always include the first two pages, last two pages, and
# two pages either side of the current page. # two pages either side of the current page.
included = set(( included = set((
@ -87,16 +103,46 @@ def _get_displayed_page_numbers(current, final):
return included return included
def _get_page_links(page_numbers, current, url_func):
"""
Given a list of page numbers and `None` page breaks,
return a list of `PageLink` objects.
"""
page_links = []
for page_number in page_numbers:
if page_number is None:
page_link = PageLink(
url=None,
number=None,
is_active=False,
is_break=True
)
else:
page_link = PageLink(
url=url_func(page_number),
number=page_number,
is_active=(page_number == current),
is_break=False
)
page_links.append(page_link)
return page_links
PageLink = namedtuple('PageLink', ['url', 'number', 'is_active', 'is_break']) PageLink = namedtuple('PageLink', ['url', 'number', 'is_active', 'is_break'])
class BasePagination(object): class BasePagination(object):
display_page_controls = False
def paginate_queryset(self, queryset, request, view): def paginate_queryset(self, queryset, request, view):
raise NotImplemented('paginate_queryset() must be implemented.') raise NotImplemented('paginate_queryset() must be implemented.')
def get_paginated_response(self, data): def get_paginated_response(self, data):
raise NotImplemented('get_paginated_response() must be implemented.') raise NotImplemented('get_paginated_response() must be implemented.')
def to_html(self):
raise NotImplemented('to_html() must be implemented to display page controls.')
class PageNumberPagination(BasePagination): class PageNumberPagination(BasePagination):
""" """
@ -161,8 +207,9 @@ class PageNumberPagination(BasePagination):
) )
raise NotFound(msg) raise NotFound(msg)
# Indicate that the browsable API should display pagination controls. if paginator.count > 1:
self.mark_as_used = True # The browsable API should display pagination controls.
self.display_page_controls = True
self.request = request self.request = request
return self.page return self.page
@ -203,31 +250,17 @@ class PageNumberPagination(BasePagination):
return replace_query_param(url, self.page_query_param, page_number) return replace_query_param(url, self.page_query_param, page_number)
def to_html(self): def to_html(self):
base_url = self.request.build_absolute_uri()
def page_number_to_url(page_number):
if page_number == 1:
return remove_query_param(base_url, self.page_query_param)
else:
return replace_query_param(base_url, self.page_query_param, page_number)
current = self.page.number current = self.page.number
final = self.page.paginator.num_pages final = self.page.paginator.num_pages
page_numbers = _get_displayed_page_numbers(current, final)
page_links = [] page_links = _get_page_links(page_numbers, current, page_number_to_url)
base_url = self.request.build_absolute_uri()
for page_number in _get_displayed_page_numbers(current, final):
if page_number is None:
page_link = PageLink(
url=None,
number=None,
is_active=False,
is_break=True
)
else:
if page_number == 1:
url = remove_query_param(base_url, self.page_query_param)
else:
url = replace_query_param(url, self.page_query_param, page_number)
page_link = PageLink(
url=url,
number=page_number,
is_active=(page_number == current),
is_break=False
)
page_links.append(page_link)
template = loader.get_template(self.template) template = loader.get_template(self.template)
context = Context({ context = Context({
@ -250,11 +283,15 @@ class LimitOffsetPagination(BasePagination):
offset_query_param = 'offset' offset_query_param = 'offset'
max_limit = None max_limit = None
template = 'rest_framework/pagination/numbers.html'
def paginate_queryset(self, queryset, request, view): def paginate_queryset(self, queryset, request, view):
self.limit = self.get_limit(request) self.limit = self.get_limit(request)
self.offset = self.get_offset(request) self.offset = self.get_offset(request)
self.count = _get_count(queryset) self.count = _get_count(queryset)
self.request = request self.request = request
if self.count > self.limit:
self.display_page_controls = True
return queryset[self.offset:self.offset + self.limit] return queryset[self.offset:self.offset + self.limit]
def get_paginated_response(self, data): def get_paginated_response(self, data):
@ -285,16 +322,45 @@ class LimitOffsetPagination(BasePagination):
except (KeyError, ValueError): except (KeyError, ValueError):
return 0 return 0
def get_next_link(self, page): def get_next_link(self):
if self.offset + self.limit >= self.count: if self.offset + self.limit >= self.count:
return None return None
url = self.request.build_absolute_uri() url = self.request.build_absolute_uri()
offset = self.offset + self.limit offset = self.offset + self.limit
return replace_query_param(url, self.offset_query_param, offset) return replace_query_param(url, self.offset_query_param, offset)
def get_previous_link(self, page): def get_previous_link(self):
if self.offset - self.limit < 0: if self.offset <= 0:
return None return None
url = self.request.build_absolute_uri() url = self.request.build_absolute_uri()
if self.offset - self.limit <= 0:
return remove_query_param(url, self.offset_query_param)
offset = self.offset - self.limit offset = self.offset - self.limit
return replace_query_param(url, self.offset_query_param, offset) return replace_query_param(url, self.offset_query_param, offset)
def to_html(self):
base_url = self.request.build_absolute_uri()
current = _divide_with_ceil(self.offset, self.limit) + 1
final = _divide_with_ceil(self.count, self.limit)
def page_number_to_url(page_number):
if page_number == 1:
return remove_query_param(base_url, self.offset_query_param)
else:
offset = self.offset + ((page_number - current) * self.limit)
return replace_query_param(base_url, self.offset_query_param, offset)
page_numbers = _get_displayed_page_numbers(current, final)
page_links = _get_page_links(page_numbers, current, page_number_to_url)
template = loader.get_template(self.template)
context = Context({
'previous_url': self.get_previous_link(),
'next_url': self.get_next_link(),
'page_links': page_links
})
return template.render(context)

View File

@ -584,6 +584,11 @@ class BrowsableAPIRenderer(BaseRenderer):
renderer_content_type += ' ;%s' % renderer.charset renderer_content_type += ' ;%s' % renderer.charset
response_headers['Content-Type'] = renderer_content_type response_headers['Content-Type'] = renderer_content_type
if hasattr(view, 'paginator') and view.paginator.display_page_controls:
paginator = view.paginator
else:
paginator = None
context = { context = {
'content': self.get_content(renderer, data, accepted_media_type, renderer_context), 'content': self.get_content(renderer, data, accepted_media_type, renderer_context),
'view': view, 'view': view,
@ -592,7 +597,7 @@ class BrowsableAPIRenderer(BaseRenderer):
'description': self.get_description(view), 'description': self.get_description(view),
'name': self.get_name(view), 'name': self.get_name(view),
'version': VERSION, 'version': VERSION,
'pager': getattr(view, 'pager', None), 'paginator': paginator,
'breadcrumblist': self.get_breadcrumbs(request), 'breadcrumblist': self.get_breadcrumbs(request),
'allowed_methods': view.allowed_methods, 'allowed_methods': view.allowed_methods,
'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes], 'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes],

View File

@ -60,6 +60,13 @@ a single block in the template.
color: #C20000; color: #C20000;
} }
.pagination>.disabled>a,
.pagination>.disabled>a:hover,
.pagination>.disabled>a:focus {
cursor: default;
pointer-events: none;
}
/*=== dabapps bootstrap styles ====*/ /*=== dabapps bootstrap styles ====*/
html { html {

View File

@ -125,9 +125,9 @@
{% endblock %} {% endblock %}
</div> </div>
{% if pager.mark_as_used %} {% if paginator %}
<nav style="float: right"> <nav style="float: right">
{% get_pagination_html pager %} {% get_pagination_html paginator %}
</nav> </nav>
{% endif %} {% endif %}