mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-26 03:23:59 +03:00
Include pagination control in browsable API
This commit is contained in:
parent
f13fcba9a9
commit
3833a5bb8a
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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 {
|
||||||
color: #c20000;
|
color: #c20000;
|
||||||
}
|
}
|
||||||
|
|
||||||
#content a span {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.request-info {
|
.request-info {
|
||||||
clear:both;
|
clear:both;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">«</span></a></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="disabled"><a href="#" aria-label="Previous"><span aria-hidden="true">«</span></a></li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for page_link in page_links %}
|
||||||
|
{% if page_link.is_break %}
|
||||||
|
<li class="disabled">
|
||||||
|
<a href="#"><span aria-hidden="true">…</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">»</span></a></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="disabled"><a href="#" aria-label="Next"><span aria-hidden="true">»</span></a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
|
@ -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=["\'])(.*)(?=["\'])')
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user