mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-02 20:54:42 +03:00
Tweaks, and add pagination controls for offset/limit.
This commit is contained in:
parent
313aa727e3
commit
d76e83dd78
|
@ -150,21 +150,21 @@ class GenericAPIView(views.APIView):
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pager(self):
|
def paginator(self):
|
||||||
if not hasattr(self, '_pager'):
|
if not hasattr(self, '_paginator'):
|
||||||
if self.pagination_class is None:
|
if self.pagination_class is None:
|
||||||
self._pager = None
|
self._paginator = None
|
||||||
else:
|
else:
|
||||||
self._pager = self.pagination_class()
|
self._paginator = self.pagination_class()
|
||||||
return self._pager
|
return self._paginator
|
||||||
|
|
||||||
def paginate_queryset(self, queryset):
|
def paginate_queryset(self, queryset):
|
||||||
if self.pager is None:
|
if self.paginator is None:
|
||||||
return queryset
|
return queryset
|
||||||
return self.pager.paginate_queryset(queryset, self.request, view=self)
|
return self.paginator.paginate_queryset(queryset, self.request, view=self)
|
||||||
|
|
||||||
def get_paginated_response(self, data):
|
def get_paginated_response(self, data):
|
||||||
return self.pager.get_paginated_response(data)
|
return self.paginator.get_paginated_response(data)
|
||||||
|
|
||||||
|
|
||||||
# Concrete view classes that provide method handlers
|
# Concrete view classes that provide method handlers
|
||||||
|
|
|
@ -29,6 +29,15 @@ def _strict_positive_int(integer_string, cutoff=None):
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def _divide_with_ceil(a, b):
|
||||||
|
"""
|
||||||
|
Returns 'a' divded by 'b', with any remainder rounded up.
|
||||||
|
"""
|
||||||
|
if a % b:
|
||||||
|
return (a / b) + 1
|
||||||
|
return a / b
|
||||||
|
|
||||||
|
|
||||||
def _get_count(queryset):
|
def _get_count(queryset):
|
||||||
"""
|
"""
|
||||||
Determine an object count, supporting either querysets or regular lists.
|
Determine an object count, supporting either querysets or regular lists.
|
||||||
|
@ -48,14 +57,21 @@ def _get_displayed_page_numbers(current, final):
|
||||||
current=14, final=16 -> [1, None, 13, 14, 15, 16]
|
current=14, final=16 -> [1, None, 13, 14, 15, 16]
|
||||||
|
|
||||||
This implementation gives one page to each side of the cursor,
|
This implementation gives one page to each side of the cursor,
|
||||||
for an implementation which gives two pages to each side of the cursor,
|
or two pages to the side when the cursor is at the edge, then
|
||||||
which is a copy of how GitHub treat pagination in their issue lists, see:
|
ensures that any breaks between non-continous page numbers never
|
||||||
|
remove only a single page.
|
||||||
|
|
||||||
|
For an alernativative implementation which gives two pages to each side of
|
||||||
|
the cursor, eg. as in GitHub issue list pagination, see:
|
||||||
|
|
||||||
https://gist.github.com/tomchristie/321140cebb1c4a558b15
|
https://gist.github.com/tomchristie/321140cebb1c4a558b15
|
||||||
"""
|
"""
|
||||||
assert current >= 1
|
assert current >= 1
|
||||||
assert final >= current
|
assert final >= current
|
||||||
|
|
||||||
|
if final <= 5:
|
||||||
|
return range(1, final + 1)
|
||||||
|
|
||||||
# We always include the first two pages, last two pages, and
|
# We always include the first two pages, last two pages, and
|
||||||
# two pages either side of the current page.
|
# two pages either side of the current page.
|
||||||
included = set((
|
included = set((
|
||||||
|
@ -87,16 +103,46 @@ def _get_displayed_page_numbers(current, final):
|
||||||
return included
|
return included
|
||||||
|
|
||||||
|
|
||||||
|
def _get_page_links(page_numbers, current, url_func):
|
||||||
|
"""
|
||||||
|
Given a list of page numbers and `None` page breaks,
|
||||||
|
return a list of `PageLink` objects.
|
||||||
|
"""
|
||||||
|
page_links = []
|
||||||
|
for page_number in page_numbers:
|
||||||
|
if page_number is None:
|
||||||
|
page_link = PageLink(
|
||||||
|
url=None,
|
||||||
|
number=None,
|
||||||
|
is_active=False,
|
||||||
|
is_break=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
page_link = PageLink(
|
||||||
|
url=url_func(page_number),
|
||||||
|
number=page_number,
|
||||||
|
is_active=(page_number == current),
|
||||||
|
is_break=False
|
||||||
|
)
|
||||||
|
page_links.append(page_link)
|
||||||
|
return page_links
|
||||||
|
|
||||||
|
|
||||||
PageLink = namedtuple('PageLink', ['url', 'number', 'is_active', 'is_break'])
|
PageLink = namedtuple('PageLink', ['url', 'number', 'is_active', 'is_break'])
|
||||||
|
|
||||||
|
|
||||||
class BasePagination(object):
|
class BasePagination(object):
|
||||||
|
display_page_controls = False
|
||||||
|
|
||||||
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.')
|
||||||
|
|
||||||
def get_paginated_response(self, data):
|
def get_paginated_response(self, data):
|
||||||
raise NotImplemented('get_paginated_response() must be implemented.')
|
raise NotImplemented('get_paginated_response() must be implemented.')
|
||||||
|
|
||||||
|
def to_html(self):
|
||||||
|
raise NotImplemented('to_html() must be implemented to display page controls.')
|
||||||
|
|
||||||
|
|
||||||
class PageNumberPagination(BasePagination):
|
class PageNumberPagination(BasePagination):
|
||||||
"""
|
"""
|
||||||
|
@ -161,8 +207,9 @@ class PageNumberPagination(BasePagination):
|
||||||
)
|
)
|
||||||
raise NotFound(msg)
|
raise NotFound(msg)
|
||||||
|
|
||||||
# Indicate that the browsable API should display pagination controls.
|
if paginator.count > 1:
|
||||||
self.mark_as_used = True
|
# The browsable API should display pagination controls.
|
||||||
|
self.display_page_controls = True
|
||||||
self.request = request
|
self.request = request
|
||||||
return self.page
|
return self.page
|
||||||
|
|
||||||
|
@ -203,31 +250,17 @@ class PageNumberPagination(BasePagination):
|
||||||
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):
|
def to_html(self):
|
||||||
|
base_url = self.request.build_absolute_uri()
|
||||||
|
def page_number_to_url(page_number):
|
||||||
|
if page_number == 1:
|
||||||
|
return remove_query_param(base_url, self.page_query_param)
|
||||||
|
else:
|
||||||
|
return replace_query_param(base_url, self.page_query_param, page_number)
|
||||||
|
|
||||||
current = self.page.number
|
current = self.page.number
|
||||||
final = self.page.paginator.num_pages
|
final = self.page.paginator.num_pages
|
||||||
|
page_numbers = _get_displayed_page_numbers(current, final)
|
||||||
page_links = []
|
page_links = _get_page_links(page_numbers, current, page_number_to_url)
|
||||||
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)
|
template = loader.get_template(self.template)
|
||||||
context = Context({
|
context = Context({
|
||||||
|
@ -250,11 +283,15 @@ class LimitOffsetPagination(BasePagination):
|
||||||
offset_query_param = 'offset'
|
offset_query_param = 'offset'
|
||||||
max_limit = None
|
max_limit = None
|
||||||
|
|
||||||
|
template = 'rest_framework/pagination/numbers.html'
|
||||||
|
|
||||||
def paginate_queryset(self, queryset, request, view):
|
def paginate_queryset(self, queryset, request, view):
|
||||||
self.limit = self.get_limit(request)
|
self.limit = self.get_limit(request)
|
||||||
self.offset = self.get_offset(request)
|
self.offset = self.get_offset(request)
|
||||||
self.count = _get_count(queryset)
|
self.count = _get_count(queryset)
|
||||||
self.request = request
|
self.request = request
|
||||||
|
if self.count > self.limit:
|
||||||
|
self.display_page_controls = True
|
||||||
return queryset[self.offset:self.offset + self.limit]
|
return queryset[self.offset:self.offset + self.limit]
|
||||||
|
|
||||||
def get_paginated_response(self, data):
|
def get_paginated_response(self, data):
|
||||||
|
@ -285,16 +322,45 @@ class LimitOffsetPagination(BasePagination):
|
||||||
except (KeyError, ValueError):
|
except (KeyError, ValueError):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def get_next_link(self, page):
|
def get_next_link(self):
|
||||||
if self.offset + self.limit >= self.count:
|
if self.offset + self.limit >= self.count:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
url = self.request.build_absolute_uri()
|
url = self.request.build_absolute_uri()
|
||||||
offset = self.offset + self.limit
|
offset = self.offset + self.limit
|
||||||
return replace_query_param(url, self.offset_query_param, offset)
|
return replace_query_param(url, self.offset_query_param, offset)
|
||||||
|
|
||||||
def get_previous_link(self, page):
|
def get_previous_link(self):
|
||||||
if self.offset - self.limit < 0:
|
if self.offset <= 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
url = self.request.build_absolute_uri()
|
url = self.request.build_absolute_uri()
|
||||||
|
|
||||||
|
if self.offset - self.limit <= 0:
|
||||||
|
return remove_query_param(url, self.offset_query_param)
|
||||||
|
|
||||||
offset = self.offset - self.limit
|
offset = self.offset - self.limit
|
||||||
return replace_query_param(url, self.offset_query_param, offset)
|
return replace_query_param(url, self.offset_query_param, offset)
|
||||||
|
|
||||||
|
def to_html(self):
|
||||||
|
base_url = self.request.build_absolute_uri()
|
||||||
|
current = _divide_with_ceil(self.offset, self.limit) + 1
|
||||||
|
final = _divide_with_ceil(self.count, self.limit)
|
||||||
|
|
||||||
|
def page_number_to_url(page_number):
|
||||||
|
if page_number == 1:
|
||||||
|
return remove_query_param(base_url, self.offset_query_param)
|
||||||
|
else:
|
||||||
|
offset = self.offset + ((page_number - current) * self.limit)
|
||||||
|
return replace_query_param(base_url, self.offset_query_param, offset)
|
||||||
|
|
||||||
|
page_numbers = _get_displayed_page_numbers(current, final)
|
||||||
|
page_links = _get_page_links(page_numbers, current, page_number_to_url)
|
||||||
|
|
||||||
|
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)
|
|
@ -584,6 +584,11 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
renderer_content_type += ' ;%s' % renderer.charset
|
renderer_content_type += ' ;%s' % renderer.charset
|
||||||
response_headers['Content-Type'] = renderer_content_type
|
response_headers['Content-Type'] = renderer_content_type
|
||||||
|
|
||||||
|
if hasattr(view, 'paginator') and view.paginator.display_page_controls:
|
||||||
|
paginator = view.paginator
|
||||||
|
else:
|
||||||
|
paginator = None
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'content': self.get_content(renderer, data, accepted_media_type, renderer_context),
|
'content': self.get_content(renderer, data, accepted_media_type, renderer_context),
|
||||||
'view': view,
|
'view': view,
|
||||||
|
@ -592,7 +597,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),
|
'paginator': paginator,
|
||||||
'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],
|
||||||
|
|
|
@ -60,6 +60,13 @@ a single block in the template.
|
||||||
color: #C20000;
|
color: #C20000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination>.disabled>a,
|
||||||
|
.pagination>.disabled>a:hover,
|
||||||
|
.pagination>.disabled>a:focus {
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
/*=== dabapps bootstrap styles ====*/
|
/*=== dabapps bootstrap styles ====*/
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|
|
@ -125,9 +125,9 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if pager.mark_as_used %}
|
{% if paginator %}
|
||||||
<nav style="float: right">
|
<nav style="float: right">
|
||||||
{% get_pagination_html pager %}
|
{% get_pagination_html paginator %}
|
||||||
</nav>
|
</nav>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user