diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index b9d487968..bd343c0dd 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 c4de30db7..4c002b168 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 36c7be481..d4a7d31a2 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 e96681932..e00309811 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -119,9 +119,18 @@ +
{% block description %} {{ description }} {% endblock %} +
+ + {% if pager.mark_as_used %} + + {% endif %} +
{{ request.method }} {{ request.get_full_path }}
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 000000000..040458104 --- /dev/null +++ b/rest_framework/templates/rest_framework/pagination/numbers.html @@ -0,0 +1,27 @@ + diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 69e03af40..bf159d8b1 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=["\'])(.*)(?=["\'])')