Commit 3833a5bb by Tom Christie

Include pagination control in browsable API

parent f13fcba9
...@@ -3,14 +3,18 @@ Pagination serializers determine the structure of the output that should ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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):
""" """
......
...@@ -592,6 +592,7 @@ class BrowsableAPIRenderer(BaseRenderer): ...@@ -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],
......
...@@ -185,10 +185,6 @@ body a:hover { ...@@ -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;
} }
...@@ -119,9 +119,18 @@ ...@@ -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>
......
<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>
...@@ -26,6 +26,23 @@ def replace_query_param(url, key, val): ...@@ -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=["\'])(.*)(?=["\'])')
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment