aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Christie2015-01-15 16:52:07 +0000
committerTom Christie2015-01-15 16:52:07 +0000
commitd76e83dd78627a0cf4bcd4b28a7710fb678d8d4e (patch)
treeeb17b5a60fb64a97b70d3aa268a3f97c6b542227
parent313aa727e3c44016e531a7af75051fc6e6d7cb96 (diff)
downloaddjango-rest-framework-d76e83dd78627a0cf4bcd4b28a7710fb678d8d4e.tar.bz2
Tweaks, and add pagination controls for offset/limit.
-rw-r--r--rest_framework/generics.py16
-rw-r--r--rest_framework/pagination.py126
-rw-r--r--rest_framework/renderers.py7
-rw-r--r--rest_framework/static/rest_framework/css/bootstrap-tweaks.css7
-rw-r--r--rest_framework/templates/rest_framework/base.html4
5 files changed, 119 insertions, 41 deletions
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index cdf6ece0..4cc4c64d 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -150,21 +150,21 @@ class GenericAPIView(views.APIView):
return queryset
@property
- def pager(self):
- if not hasattr(self, '_pager'):
+ def paginator(self):
+ if not hasattr(self, '_paginator'):
if self.pagination_class is None:
- self._pager = None
+ self._paginator = None
else:
- self._pager = self.pagination_class()
- return self._pager
+ self._paginator = self.pagination_class()
+ return self._paginator
def paginate_queryset(self, queryset):
- if self.pager is None:
+ if self.paginator is None:
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):
- return self.pager.get_paginated_response(data)
+ return self.paginator.get_paginated_response(data)
# Concrete view classes that provide method handlers
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index 69d0f77d..2b78f1f7 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -29,6 +29,15 @@ def _strict_positive_int(integer_string, cutoff=None):
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):
"""
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]
This implementation gives one page to each side of the cursor,
- for an implementation which gives two pages to each side of the cursor,
- which is a copy of how GitHub treat pagination in their issue lists, see:
+ or two pages to the side when the cursor is at the edge, then
+ 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
"""
assert current >= 1
assert final >= current
+ if final <= 5:
+ return range(1, final + 1)
+
# We always include the first two pages, last two pages, and
# two pages either side of the current page.
included = set((
@@ -87,16 +103,46 @@ def _get_displayed_page_numbers(current, final):
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'])
class BasePagination(object):
+ display_page_controls = False
+
def paginate_queryset(self, queryset, request, view):
raise NotImplemented('paginate_queryset() must be implemented.')
def get_paginated_response(self, data):
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):
"""
@@ -161,8 +207,9 @@ class PageNumberPagination(BasePagination):
)
raise NotFound(msg)
- # Indicate that the browsable API should display pagination controls.
- self.mark_as_used = True
+ if paginator.count > 1:
+ # The browsable API should display pagination controls.
+ self.display_page_controls = True
self.request = request
return self.page
@@ -203,31 +250,17 @@ class PageNumberPagination(BasePagination):
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
- )
+ def page_number_to_url(page_number):
+ if page_number == 1:
+ return remove_query_param(base_url, self.page_query_param)
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)
+ return replace_query_param(base_url, self.page_query_param, page_number)
+
+ current = self.page.number
+ final = self.page.paginator.num_pages
+ 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({
@@ -250,11 +283,15 @@ class LimitOffsetPagination(BasePagination):
offset_query_param = 'offset'
max_limit = None
+ template = 'rest_framework/pagination/numbers.html'
+
def paginate_queryset(self, queryset, request, view):
self.limit = self.get_limit(request)
self.offset = self.get_offset(request)
self.count = _get_count(queryset)
self.request = request
+ if self.count > self.limit:
+ self.display_page_controls = True
return queryset[self.offset:self.offset + self.limit]
def get_paginated_response(self, data):
@@ -285,16 +322,45 @@ class LimitOffsetPagination(BasePagination):
except (KeyError, ValueError):
return 0
- def get_next_link(self, page):
+ def get_next_link(self):
if self.offset + self.limit >= self.count:
return None
+
url = self.request.build_absolute_uri()
offset = self.offset + self.limit
return replace_query_param(url, self.offset_query_param, offset)
- def get_previous_link(self, page):
- if self.offset - self.limit < 0:
+ def get_previous_link(self):
+ if self.offset <= 0:
return None
+
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
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) \ No newline at end of file
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 4c002b16..4c46b049 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -584,6 +584,11 @@ class BrowsableAPIRenderer(BaseRenderer):
renderer_content_type += ' ;%s' % renderer.charset
response_headers['Content-Type'] = renderer_content_type
+ if hasattr(view, 'paginator') and view.paginator.display_page_controls:
+ paginator = view.paginator
+ else:
+ paginator = None
+
context = {
'content': self.get_content(renderer, data, accepted_media_type, renderer_context),
'view': view,
@@ -592,7 +597,7 @@ class BrowsableAPIRenderer(BaseRenderer):
'description': self.get_description(view),
'name': self.get_name(view),
'version': VERSION,
- 'pager': getattr(view, 'pager', None),
+ 'paginator': paginator,
'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 d4a7d31a..15b42178 100644
--- a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
+++ b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
@@ -60,6 +60,13 @@ a single block in the template.
color: #C20000;
}
+.pagination>.disabled>a,
+.pagination>.disabled>a:hover,
+.pagination>.disabled>a:focus {
+ cursor: default;
+ pointer-events: none;
+}
+
/*=== dabapps bootstrap styles ====*/
html {
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index e0030981..877387f2 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -125,9 +125,9 @@
{% endblock %}
</div>
- {% if pager.mark_as_used %}
+ {% if paginator %}
<nav style="float: right">
- {% get_pagination_html pager %}
+ {% get_pagination_html paginator %}
</nav>
{% endif %}