mirror of
				https://github.com/encode/django-rest-framework.git
				synced 2025-11-04 18:08:03 +03:00 
			
		
		
		
	Add paging controls
This commit is contained in:
		
							parent
							
								
									0822c9e558
								
							
						
					
					
						commit
						43d983fae8
					
				| 
						 | 
				
			
			@ -133,9 +133,14 @@ def _decode_cursor(encoded):
 | 
			
		|||
    try:
 | 
			
		||||
        querystring = b64decode(encoded.encode('ascii')).decode('ascii')
 | 
			
		||||
        tokens = urlparse.parse_qs(querystring, keep_blank_values=True)
 | 
			
		||||
        offset = _positive_int(tokens['offset'][0])
 | 
			
		||||
        reverse = bool(int(tokens['reverse'][0]))
 | 
			
		||||
        position = tokens.get('position', [None])[0]
 | 
			
		||||
 | 
			
		||||
        offset = tokens.get('o', ['0'])[0]
 | 
			
		||||
        offset = _positive_int(offset)
 | 
			
		||||
 | 
			
		||||
        reverse = tokens.get('r', ['0'])[0]
 | 
			
		||||
        reverse = bool(int(reverse))
 | 
			
		||||
 | 
			
		||||
        position = tokens.get('p', [None])[0]
 | 
			
		||||
    except (TypeError, ValueError):
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -146,12 +151,13 @@ def _encode_cursor(cursor):
 | 
			
		|||
    """
 | 
			
		||||
    Given a Cursor instance, return an encoded string representation.
 | 
			
		||||
    """
 | 
			
		||||
    tokens = {
 | 
			
		||||
        'offset': str(cursor.offset),
 | 
			
		||||
        'reverse': '1' if cursor.reverse else '0',
 | 
			
		||||
    }
 | 
			
		||||
    tokens = {}
 | 
			
		||||
    if cursor.offset != 0:
 | 
			
		||||
        tokens['o'] = str(cursor.offset)
 | 
			
		||||
    if cursor.reverse:
 | 
			
		||||
        tokens['r'] = '1'
 | 
			
		||||
    if cursor.position is not None:
 | 
			
		||||
        tokens['position'] = cursor.position
 | 
			
		||||
        tokens['p'] = cursor.position
 | 
			
		||||
 | 
			
		||||
    querystring = urlparse.urlencode(tokens, doseq=True)
 | 
			
		||||
    return b64encode(querystring.encode('ascii')).decode('ascii')
 | 
			
		||||
| 
						 | 
				
			
			@ -430,10 +436,12 @@ class CursorPagination(BasePagination):
 | 
			
		|||
    # Determine how/if True, False and None positions work - do the string
 | 
			
		||||
    # encodings work with Django queryset filters?
 | 
			
		||||
    # Consider a max offset cap.
 | 
			
		||||
    # Tidy up the `get_ordering` API (eg remove queryset from it)
 | 
			
		||||
    cursor_query_param = 'cursor'
 | 
			
		||||
    page_size = api_settings.PAGINATE_BY
 | 
			
		||||
    invalid_cursor_message = _('Invalid cursor')
 | 
			
		||||
    ordering = None
 | 
			
		||||
    template = 'rest_framework/pagination/previous_and_next.html'
 | 
			
		||||
 | 
			
		||||
    def paginate_queryset(self, queryset, request, view=None):
 | 
			
		||||
        self.base_url = request.build_absolute_uri()
 | 
			
		||||
| 
						 | 
				
			
			@ -452,17 +460,22 @@ class CursorPagination(BasePagination):
 | 
			
		|||
 | 
			
		||||
        # Cursor pagination always enforces an ordering.
 | 
			
		||||
        if reverse:
 | 
			
		||||
            queryset = queryset.order_by(_reverse_ordering(self.ordering))
 | 
			
		||||
            queryset = queryset.order_by(*_reverse_ordering(self.ordering))
 | 
			
		||||
        else:
 | 
			
		||||
            queryset = queryset.order_by(self.ordering)
 | 
			
		||||
            queryset = queryset.order_by(*self.ordering)
 | 
			
		||||
 | 
			
		||||
        # If we have a cursor with a fixed position then filter by that.
 | 
			
		||||
        if current_position is not None:
 | 
			
		||||
            primary_ordering_attr = self.ordering[0].lstrip('-')
 | 
			
		||||
            if self.cursor.reverse:
 | 
			
		||||
                kwargs = {primary_ordering_attr + '__lt': current_position}
 | 
			
		||||
            order = self.ordering[0]
 | 
			
		||||
            is_reversed = order.startswith('-')
 | 
			
		||||
            order_attr = order.lstrip('-')
 | 
			
		||||
 | 
			
		||||
            # Test for: (cursor reversed) XOR (queryset reversed)
 | 
			
		||||
            if self.cursor.reverse != is_reversed:
 | 
			
		||||
                kwargs = {order_attr + '__lt': current_position}
 | 
			
		||||
            else:
 | 
			
		||||
                kwargs = {primary_ordering_attr + '__gt': current_position}
 | 
			
		||||
                kwargs = {order_attr + '__gt': current_position}
 | 
			
		||||
 | 
			
		||||
            queryset = queryset.filter(**kwargs)
 | 
			
		||||
 | 
			
		||||
        # If we have an offset cursor then offset the entire page by that amount.
 | 
			
		||||
| 
						 | 
				
			
			@ -501,6 +514,11 @@ class CursorPagination(BasePagination):
 | 
			
		|||
            if self.has_previous:
 | 
			
		||||
                self.previous_position = current_position
 | 
			
		||||
 | 
			
		||||
        # Display page controls in the browsable API if there is more
 | 
			
		||||
        # than one page.
 | 
			
		||||
        if self.has_previous or self.has_next:
 | 
			
		||||
            self.display_page_controls = True
 | 
			
		||||
 | 
			
		||||
        return self.page
 | 
			
		||||
 | 
			
		||||
    def get_next_link(self):
 | 
			
		||||
| 
						 | 
				
			
			@ -642,5 +660,23 @@ class CursorPagination(BasePagination):
 | 
			
		|||
        return tuple(ordering)
 | 
			
		||||
 | 
			
		||||
    def _get_position_from_instance(self, instance, ordering):
 | 
			
		||||
        attr = getattr(instance, ordering[0])
 | 
			
		||||
        attr = getattr(instance, ordering[0].lstrip('-'))
 | 
			
		||||
        return six.text_type(attr)
 | 
			
		||||
 | 
			
		||||
    def get_paginated_response(self, data):
 | 
			
		||||
        return Response(OrderedDict([
 | 
			
		||||
            ('next', self.get_next_link()),
 | 
			
		||||
            ('previous', self.get_previous_link()),
 | 
			
		||||
            ('results', data)
 | 
			
		||||
        ]))
 | 
			
		||||
 | 
			
		||||
    def get_html_context(self):
 | 
			
		||||
        return {
 | 
			
		||||
            'previous_url': self.get_previous_link(),
 | 
			
		||||
            'next_url': self.get_next_link()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def to_html(self):
 | 
			
		||||
        template = loader.get_template(self.template)
 | 
			
		||||
        context = Context(self.get_html_context())
 | 
			
		||||
        return template.render(context)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -63,10 +63,20 @@ a single block in the template.
 | 
			
		|||
.pagination>.disabled>a,
 | 
			
		||||
.pagination>.disabled>a:hover,
 | 
			
		||||
.pagination>.disabled>a:focus {
 | 
			
		||||
  cursor: default;
 | 
			
		||||
  cursor: not-allowed;
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pager>.disabled>a,
 | 
			
		||||
.pager>.disabled>a:hover,
 | 
			
		||||
.pager>.disabled>a:focus {
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pager .next {
 | 
			
		||||
  margin-left: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*=== dabapps bootstrap styles ====*/
 | 
			
		||||
 | 
			
		||||
html {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
<ul class="pager">
 | 
			
		||||
{% if previous_url %}
 | 
			
		||||
    <li class="previous"><a href="{{ previous_url }}">« Previous</a></li>
 | 
			
		||||
{% else %}
 | 
			
		||||
    <li class="previous disabled"><a href="#">« Previous</a></li>
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% if next_url %}
 | 
			
		||||
    <li class="next"><a href="{{ next_url }}">Next »</a></li>
 | 
			
		||||
{% else %}
 | 
			
		||||
    <li class="next disabled"><a href="#">Next »</li>
 | 
			
		||||
{% endif %}
 | 
			
		||||
</ul>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
# coding: utf-8
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
from rest_framework import exceptions, generics, pagination, serializers, status, filters
 | 
			
		||||
from rest_framework.request import Request
 | 
			
		||||
| 
						 | 
				
			
			@ -471,7 +472,7 @@ class TestCursorPagination:
 | 
			
		|||
                    if item.created < int(created__lt)
 | 
			
		||||
                ])
 | 
			
		||||
 | 
			
		||||
            def order_by(self, ordering):
 | 
			
		||||
            def order_by(self, *ordering):
 | 
			
		||||
                if ordering[0].startswith('-'):
 | 
			
		||||
                    return MockQuerySet(list(reversed(self.items)))
 | 
			
		||||
                return self
 | 
			
		||||
| 
						 | 
				
			
			@ -614,6 +615,8 @@ class TestCursorPagination:
 | 
			
		|||
        assert current == [1, 1, 1, 1, 1]
 | 
			
		||||
        assert next == [1, 2, 3, 4, 4]
 | 
			
		||||
 | 
			
		||||
        assert isinstance(self.pagination.to_html(), type(''))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_displayed_page_numbers():
 | 
			
		||||
    """
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user