Repository: encode/django-rest-framework

Issue:
IndexError for CursorPagination when queryset changes
## Checklist

- [X] I have verified that that issue exists against the `master` branch of Django REST framework.
- [x] I have searched for similar issues in both open and closed tickets and cannot find a duplicate.
- [x] This is not a usage question. (Those should be directed to the [discussion group](https://groups.google.com/forum/#!forum/django-rest-framework) instead.)
- [x] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](https://www.django-rest-framework.org/community/third-party-packages/#about-third-party-packages) where possible.)
- [x] I have reduced the issue to the simplest possible case.
- [x] I have included a failing test as a pull request. (If you are unable to do so we can still accept the issue.)

## Steps to reproduce

1. Enable CursorPagination
2. Make enough items that there is pagination 
3. Go to page 2
4. Delete all items within the shell
5. Refresh the page
6. See error

## Expected behavior
To be honest I'm not sure, maybe an empty page.

## Actual behavior

```
Traceback (most recent call last):
  File "/var/env/lib/python3.6/site-packages/django/contrib/staticfiles/handlers.py", line 65, in __call__
    return self.application(environ, start_response)
  File "/var/env/lib/python3.6/site-packages/django/core/handlers/wsgi.py", line 142, in __call__
    response = self.get_response(request)
  File "/var/env/lib/python3.6/site-packages/django/core/handlers/base.py", line 78, in get_response
    response = self._middleware_chain(request)
  File "/var/env/lib/python3.6/site-packages/django/core/handlers/exception.py", line 36, in inner
    response = response_for_exception(request, exc)
  File "/var/env/lib/python3.6/site-packages/django/core/handlers/exception.py", line 90, in response_for_exception
    response = handle_uncaught_exception(request, get_resolver(get_urlconf()), sys.exc_info())
  File "/var/env/lib/python3.6/site-packages/django/core/handlers/exception.py", line 125, in handle_uncaught_exception
    return debug.technical_500_response(request, *exc_info)
  File "/var/env/lib/python3.6/site-packages/django_extensions/management/technical_response.py", line 37, in null_technical_500_response
    six.reraise(exc_type, exc_value, tb)
  File "/var/env/lib/python3.6/site-packages/six.py", line 692, in reraise
    raise value.with_traceback(tb)
  File "/var/env/lib/python3.6/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "/var/env/lib/python3.6/site-packages/django/core/handlers/base.py", line 126, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/var/env/lib/python3.6/site-packages/django/core/handlers/base.py", line 124, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/var/env/lib/python3.6/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/var/env/lib/python3.6/site-packages/rest_framework/viewsets.py", line 116, in view
    return self.dispatch(request, *args, **kwargs)
  File "/var/env/lib/python3.6/site-packages/rest_framework/views.py", line 495, in dispatch
    response = self.handle_exception(exc)
  File "/var/env/lib/python3.6/site-packages/rest_framework/views.py", line 455, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/var/env/lib/python3.6/site-packages/rest_framework/views.py", line 492, in dispatch
    response = handler(request, *args, **kwargs)
  File "/var/env/lib/python3.6/site-packages/rest_framework/mixins.py", line 45, in list
    return self.get_paginated_response(serializer.data)
  File "/var/env/lib/python3.6/site-packages/rest_framework/generics.py", line 180, in get_paginated_response
    return self.paginator.get_paginated_response(data)
  File "/var/env/lib/python3.6/site-packages/rest_framework/pagination.py", line 781, in get_paginated_response
    ('previous', self.get_previous_link()),
  File "/var/env/lib/python3.6/site-packages/rest_framework/pagination.py", line 643, in get_previous_link
    compare = self._get_position_from_instance(self.page[0], self.ordering)
IndexError: list index out of range
```


Discussion:
I'm going to try and have a go at it. If there's no news from me by April 15, consider I'm not on it anymore.
For the record I can't repoduce when following the exact steps of the ticket, but if I try to go back to page 1 then I have the same traceback.

Resolving patch:
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -589,7 +589,7 @@ def get_next_link(self):
         if not self.has_next:
             return None
 
-        if self.cursor and self.cursor.reverse and self.cursor.offset != 0:
+        if self.page and self.cursor and self.cursor.reverse and self.cursor.offset != 0:
             # If we're reversing direction and we have an offset cursor
             # then we cannot use the first position we find as a marker.
             compare = self._get_position_from_instance(self.page[-1], self.ordering)
@@ -597,12 +597,14 @@ def get_next_link(self):
             compare = self.next_position
         offset = 0
 
+        has_item_with_unique_position = False
         for item in reversed(self.page):
             position = self._get_position_from_instance(item, self.ordering)
             if position != compare:
                 # The item in this position and the item following it
                 # have different positions. We can use this position as
                 # our marker.
+                has_item_with_unique_position = True
                 break
 
             # The item in this position has the same position as the item
@@ -611,7 +613,7 @@ def get_next_link(self):
             compare = position
             offset += 1
 
-        else:
+        if self.page and not has_item_with_unique_position:
             # There were no unique positions in the page.
             if not self.has_previous:
                 # We are on the first page.
@@ -630,6 +632,9 @@ def get_next_link(self):
                 offset = self.cursor.offset + self.page_size
                 position = self.previous_position
 
+        if not self.page:
+            position = self.next_position
+
         cursor = Cursor(offset=offset, reverse=False, position=position)
         return self.encode_cursor(cursor)
 
@@ -637,7 +642,7 @@ def get_previous_link(self):
         if not self.has_previous:
             return None
 
-        if self.cursor and not self.cursor.reverse and self.cursor.offset != 0:
+        if self.page and self.cursor and not self.cursor.reverse and self.cursor.offset != 0:
             # If we're reversing direction and we have an offset cursor
             # then we cannot use the first position we find as a marker.
             compare = self._get_position_from_instance(self.page[0], self.ordering)
@@ -645,12 +650,14 @@ def get_previous_link(self):
             compare = self.previous_position
         offset = 0
 
+        has_item_with_unique_position = False
         for item in self.page:
             position = self._get_position_from_instance(item, self.ordering)
             if position != compare:
                 # The item in this position and the item following it
                 # have different positions. We can use this position as
                 # our marker.
+                has_item_with_unique_position = True
                 break
 
             # The item in this position has the same position as the item
@@ -659,7 +666,7 @@ def get_previous_link(self):
             compare = position
             offset += 1
 
-        else:
+        if self.page and not has_item_with_unique_position:
             # There were no unique positions in the page.
             if not self.has_next:
                 # We are on the final page.
@@ -678,6 +685,9 @@ def get_previous_link(self):
                 offset = 0
                 position = self.next_position
 
+        if not self.page:
+            position = self.previous_position
+
         cursor = Cursor(offset=offset, reverse=True, position=position)
         return self.encode_cursor(cursor)
 


Test patch:
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -634,6 +634,52 @@ def test_cursor_pagination(self):
 
         assert isinstance(self.pagination.to_html(), six.text_type)
 
+    def test_cursor_pagination_current_page_empty_forward(self):
+        # Regression test for #6504
+        self.pagination.base_url = "/"
+
+        # We have a cursor on the element at position 100, but this element doesn't exist
+        # anymore.
+        cursor = pagination.Cursor(reverse=False, offset=0, position=100)
+        url = self.pagination.encode_cursor(cursor)
+        self.pagination.base_url = "/"
+
+        # Loading the page with this cursor doesn't crash
+        (previous, current, next, previous_url, next_url) = self.get_pages(url)
+
+        # The previous url doesn't crash either
+        (previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
+
+        # And point to things that are not completely off.
+        assert previous == [7, 7, 7, 8, 9]
+        assert current == [9, 9, 9, 9, 9]
+        assert next == []
+        assert previous_url is not None
+        assert next_url is not None
+
+    def test_cursor_pagination_current_page_empty_reverse(self):
+        # Regression test for #6504
+        self.pagination.base_url = "/"
+
+        # We have a cursor on the element at position 100, but this element doesn't exist
+        # anymore.
+        cursor = pagination.Cursor(reverse=True, offset=0, position=100)
+        url = self.pagination.encode_cursor(cursor)
+        self.pagination.base_url = "/"
+
+        # Loading the page with this cursor doesn't crash
+        (previous, current, next, previous_url, next_url) = self.get_pages(url)
+
+        # The previous url doesn't crash either
+        (previous, current, next, previous_url, next_url) = self.get_pages(next_url)
+
+        # And point to things that are not completely off.
+        assert previous == [7, 7, 7, 7, 8]
+        assert current == []
+        assert next is None
+        assert previous_url is not None
+        assert next_url is None
+
     def test_cursor_pagination_with_page_size(self):
         (previous, current, next, previous_url, next_url) = self.get_pages('/?page_size=20')