Include pagination control in browsable API

This commit is contained in:
Tom Christie 2015-01-14 16:51:26 +00:00
parent f13fcba9a9
commit 3833a5bb8a
6 changed files with 143 additions and 5 deletions

View File

@ -3,14 +3,18 @@ Pagination serializers determine the structure of the output that should
be used for paginated responses. be used for paginated responses.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import namedtuple
from django.core.paginator import InvalidPage, Paginator as DjangoPaginator from django.core.paginator import InvalidPage, Paginator as DjangoPaginator
from django.template import Context, loader
from django.utils import six from django.utils import six
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from rest_framework.compat import OrderedDict from rest_framework.compat import OrderedDict
from rest_framework.exceptions import NotFound from rest_framework.exceptions import NotFound
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.templatetags.rest_framework import replace_query_param from rest_framework.templatetags.rest_framework import (
replace_query_param, remove_query_param
)
def _strict_positive_int(integer_string, cutoff=None): def _strict_positive_int(integer_string, cutoff=None):
@ -35,6 +39,49 @@ def _get_count(queryset):
return len(queryset) return len(queryset)
def _get_displayed_page_numbers(current, final):
"""
This utility function determines a list of page numbers to display.
This gives us a nice contextually relevant set of page numbers.
For example:
current=14, final=16 -> [1, None, 13, 14, 15, 16]
"""
assert current >= 1
assert final >= current
# We always include the first two pages, last two pages, and
# two pages either side of the current page.
included = set((
1,
current - 1, current, current + 1,
final
))
# If the break would only exclude a single page number then we
# may as well include the page number instead of the break.
if current == 4:
included.add(2)
if current == final - 3:
included.add(final - 1)
# Now sort the page numbers and drop anything outside the limits.
included = [
idx for idx in sorted(list(included))
if idx > 0 and idx <= final
]
# Finally insert any `...` breaks
if current > 4:
included.insert(1, None)
if current < final - 3:
included.insert(len(included) - 1, None)
return included
PageLink = namedtuple('PageLink', ['url', 'number', 'is_active', 'is_break'])
class BasePagination(object): class BasePagination(object):
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.')
@ -66,6 +113,8 @@ class PageNumberPagination(BasePagination):
# Only relevant if 'paginate_by_param' has also been set. # Only relevant if 'paginate_by_param' has also been set.
max_paginate_by = api_settings.MAX_PAGINATE_BY max_paginate_by = api_settings.MAX_PAGINATE_BY
template = 'rest_framework/pagination/numbers.html'
def paginate_queryset(self, queryset, request, view): def paginate_queryset(self, queryset, request, view):
""" """
Paginate a queryset if required, either returning a Paginate a queryset if required, either returning a
@ -104,6 +153,8 @@ class PageNumberPagination(BasePagination):
) )
raise NotFound(msg) raise NotFound(msg)
# Indicate that the browsable API should display pagination controls.
self.mark_as_used = True
self.request = request self.request = request
return self.page return self.page
@ -139,8 +190,45 @@ class PageNumberPagination(BasePagination):
return None return None
url = self.request.build_absolute_uri() url = self.request.build_absolute_uri()
page_number = self.page.previous_page_number() page_number = self.page.previous_page_number()
if page_number == 1:
return remove_query_param(url, self.page_query_param)
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):
current = self.page.number
final = self.page.paginator.num_pages
page_links = []
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)
context = Context({
'previous_url': self.get_previous_link(),
'next_url': self.get_next_link(),
'page_links': page_links
})
return template.render(context)
class LimitOffsetPagination(BasePagination): class LimitOffsetPagination(BasePagination):
""" """

View File

@ -592,6 +592,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),
'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

@ -185,10 +185,6 @@ body a:hover {
color: #c20000; color: #c20000;
} }
#content a span {
text-decoration: underline;
}
.request-info { .request-info {
clear:both; clear:both;
} }

View File

@ -119,9 +119,18 @@
<div class="page-header"> <div class="page-header">
<h1>{{ name }}</h1> <h1>{{ name }}</h1>
</div> </div>
<div style="float:left">
{% block description %} {% block description %}
{{ description }} {{ description }}
{% endblock %} {% endblock %}
</div>
{% if pager.mark_as_used %}
<nav style="float: right">
{% get_pagination_html pager %}
</nav>
{% endif %}
<div class="request-info" style="clear: both" > <div class="request-info" style="clear: both" >
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre> <pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
</div> </div>

View File

@ -0,0 +1,27 @@
<ul class="pagination" style="margin: 5px 0 10px 0">
{% if previous_url %}
<li><a href="{{ previous_url }}" aria-label="Previous"><span aria-hidden="true">&laquo;</span></a></li>
{% else %}
<li class="disabled"><a href="#" aria-label="Previous"><span aria-hidden="true">&laquo;</span></a></li>
{% endif %}
{% for page_link in page_links %}
{% if page_link.is_break %}
<li class="disabled">
<a href="#"><span aria-hidden="true">&hellip;</span></a>
</li>
{% else %}
{% if page_link.is_active %}
<li class="active"><a href="{{ page_link.url }}">{{ page_link.number }}</a></li>
{% else %}
<li><a href="{{ page_link.url }}">{{ page_link.number }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if next_url %}
<li><a href="{{ next_url }}" aria-label="Next"><span aria-hidden="true">&raquo;</span></a></li>
{% else %}
<li class="disabled"><a href="#" aria-label="Next"><span aria-hidden="true">&raquo;</span></a></li>
{% endif %}
</ul>

View File

@ -26,6 +26,23 @@ def replace_query_param(url, key, val):
return urlparse.urlunsplit((scheme, netloc, path, query, fragment)) return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
def remove_query_param(url, key):
"""
Given a URL and a key/val pair, set or replace an item in the query
parameters of the URL, and return the new URL.
"""
(scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
query_dict = QueryDict(query).copy()
query_dict.pop(key, None)
query = query_dict.urlencode()
return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
@register.simple_tag
def get_pagination_html(pager):
return pager.to_html()
# Regex for adding classes to html snippets # Regex for adding classes to html snippets
class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])') class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')