Skip to content

Commit

Permalink
Tweaks, and add pagination controls for offset/limit.
Browse files Browse the repository at this point in the history
  • Loading branch information
tomchristie committed Jan 15, 2015
1 parent 313aa72 commit d76e83d
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 41 deletions.
16 changes: 8 additions & 8 deletions rest_framework/generics.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,21 +150,21 @@ def filter_queryset(self, queryset):
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
Expand Down
126 changes: 96 additions & 30 deletions rest_framework/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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((
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -161,8 +207,9 @@ def paginate_queryset(self, queryset, request, view):
)
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

Expand Down Expand Up @@ -203,31 +250,17 @@ def get_previous_link(self):
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({
Expand All @@ -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):
Expand Down Expand Up @@ -285,16 +322,45 @@ def get_offset(self, request):
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)
7 changes: 6 additions & 1 deletion rest_framework/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,11 @@ def get_context(self, data, accepted_media_type, renderer_context):
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,
Expand All @@ -592,7 +597,7 @@ def get_context(self, data, accepted_media_type, renderer_context):
'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],
Expand Down
7 changes: 7 additions & 0 deletions rest_framework/static/rest_framework/css/bootstrap-tweaks.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions rest_framework/templates/rest_framework/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ <h1>{{ name }}</h1>
{% endblock %}
</div>

{% if pager.mark_as_used %}
{% if paginator %}
<nav style="float: right">
{% get_pagination_html pager %}
{% get_pagination_html paginator %}
</nav>
{% endif %}

Expand Down

0 comments on commit d76e83d

Please sign in to comment.