aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework
diff options
context:
space:
mode:
authorTom Christie2015-01-14 16:51:26 +0000
committerTom Christie2015-01-14 16:51:26 +0000
commit3833a5bb8a9174e5fb09dac59a964eff24b6065e (patch)
treef0d343961fc83ead4ea79566afba4df7baf1bada /rest_framework
parentf13fcba9a9f41f7e00e0ea8956fcc65ca168c76c (diff)
downloaddjango-rest-framework-3833a5bb8a9174e5fb09dac59a964eff24b6065e.tar.bz2
Include pagination control in browsable API
Diffstat (limited to 'rest_framework')
-rw-r--r--rest_framework/pagination.py90
-rw-r--r--rest_framework/renderers.py1
-rw-r--r--rest_framework/static/rest_framework/css/bootstrap-tweaks.css4
-rw-r--r--rest_framework/templates/rest_framework/base.html9
-rw-r--r--rest_framework/templates/rest_framework/pagination/numbers.html27
-rw-r--r--rest_framework/templatetags/rest_framework.py17
6 files changed, 143 insertions, 5 deletions
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index b9d48796..bd343c0d 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -3,14 +3,18 @@ Pagination serializers determine the structure of the output that should
be used for paginated responses.
"""
from __future__ import unicode_literals
+from collections import namedtuple
from django.core.paginator import InvalidPage, Paginator as DjangoPaginator
+from django.template import Context, loader
from django.utils import six
from django.utils.translation import ugettext as _
from rest_framework.compat import OrderedDict
from rest_framework.exceptions import NotFound
from rest_framework.response import Response
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):
@@ -35,6 +39,49 @@ def _get_count(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):
def paginate_queryset(self, queryset, request, view):
raise NotImplemented('paginate_queryset() must be implemented.')
@@ -66,6 +113,8 @@ class PageNumberPagination(BasePagination):
# Only relevant if 'paginate_by_param' has also been set.
max_paginate_by = api_settings.MAX_PAGINATE_BY
+ template = 'rest_framework/pagination/numbers.html'
+
def paginate_queryset(self, queryset, request, view):
"""
Paginate a queryset if required, either returning a
@@ -104,6 +153,8 @@ class PageNumberPagination(BasePagination):
)
raise NotFound(msg)
+ # Indicate that the browsable API should display pagination controls.
+ self.mark_as_used = True
self.request = request
return self.page
@@ -139,8 +190,45 @@ class PageNumberPagination(BasePagination):
return None
url = self.request.build_absolute_uri()
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)
+ 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):
"""
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index c4de30db..4c002b16 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -592,6 +592,7 @@ class BrowsableAPIRenderer(BaseRenderer):
'description': self.get_description(view),
'name': self.get_name(view),
'version': VERSION,
+ 'pager': getattr(view, 'pager', None),
'breadcrumblist': self.get_breadcrumbs(request),
'allowed_methods': view.allowed_methods,
'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes],
diff --git a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
index 36c7be48..d4a7d31a 100644
--- a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
+++ b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
@@ -185,10 +185,6 @@ body a:hover {
color: #c20000;
}
-#content a span {
- text-decoration: underline;
- }
-
.request-info {
clear:both;
}
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index e9668193..e0030981 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -119,9 +119,18 @@
<div class="page-header">
<h1>{{ name }}</h1>
</div>
+ <div style="float:left">
{% block description %}
{{ description }}
{% endblock %}
+ </div>
+
+ {% if pager.mark_as_used %}
+ <nav style="float: right">
+ {% get_pagination_html pager %}
+ </nav>
+ {% endif %}
+
<div class="request-info" style="clear: both" >
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
</div>
diff --git a/rest_framework/templates/rest_framework/pagination/numbers.html b/rest_framework/templates/rest_framework/pagination/numbers.html
new file mode 100644
index 00000000..04045810
--- /dev/null
+++ b/rest_framework/templates/rest_framework/pagination/numbers.html
@@ -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>
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
index 69e03af4..bf159d8b 100644
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -26,6 +26,23 @@ def replace_query_param(url, key, val):
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
class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')