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 paginate_queryset(self, queryset, request, view=None): self.page_size = self.get_page_size(request) if not self.page_size: return None self.base_url = request.build_absolute_uri() self.ordering = self.get_ordering(request, queryset, view) if 'id' not in self.ordering and '-id' not in self.ordering: ordering = list(self.ordering) ordering.append('id') self.ordering = tuple(ordering) self.cursor = self.decode_cursor(request) if self.cursor is None: (offset, reverse, current_position) = (0, False, None) else: (offset, reverse, current_position) = self.cursor try: if current_position: current_position = json.loads(current_position) except TypeError: current_position = None # Cursor pagination always enforces an ordering. if reverse: queryset = queryset.order_by( *drf_p._reverse_ordering(self.ordering)) else: 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: def filter_segment(segment_len): kwargs = {} for idx in range(segment_len): order = self.ordering[idx] is_reversed = order.startswith('-') order_attr = order.lstrip('-') attr_value = self.field_to_python( queryset, order_attr, current_position[order_attr]) # Test for: (cursor reversed) XOR (queryset reversed) if idx < segment_len - 1: # if this is the last field in this segment if attr_value == self.NONE_VALUE: kwargs[order_attr + '__isnull'] = True else: kwargs[order_attr] = attr_value elif self.cursor.reverse != is_reversed: if attr_value == self.NONE_VALUE: kwargs[order_attr + '__isnull'] = False else: kwargs[order_attr + '__lt'] = attr_value else: if attr_value == self.NONE_VALUE: # It just doesn't go higher than None... so we use some impossible to reach filter kwargs['id'] = -1 else: kwargs[order_attr + '__gt'] = attr_value return Q(**kwargs) fltr = Q() for seg_len in range(len(self.ordering)): fltr = fltr | filter_segment(seg_len + 1) queryset = queryset.filter(fltr) # If we have an offset cursor then offset the entire page by that amount. # We also always fetch an extra item in order to determine if there is a # page following on from this one. results = list(queryset[offset:offset + self.page_size + 1]) self.page = list(results[:self.page_size]) if not self.page: # In rest_framework/pagination.py/get_previous_link is expected that self.page is not empty # if cursor offset != 0 self.cursor = drf_p.Cursor(offset=0, reverse=False, position=None) # DF: contrary to DRF's implementation we always provide the "prev" and "next" links as there may appear # new records before / after the ones we already read. Mechanism for detecting whether they actually appeared # is another matter altogether :) if current_position is None and results: self.cursor = drf_p.Cursor(offset=0, reverse=False, position=None) current_position = self._get_position_from_instance( results[0], self.ordering) elif isinstance(current_position, dict): current_position = json.dumps(current_position) # Determine the position of the final item following the page. has_following_position = True # len(results) > len(self.page) # DF: contrary to DRF's implementation we always provide the "next" link as there may appear new records after # the last one read following_position = self._get_position_from_instance( results[-1], self.ordering) if results else None if reverse: # If we have a reverse queryset, then the query ordering was in reverse # so we need to reverse the items again before returning them to the user. self.page = list(reversed(self.page)) # Determine next and previous positions for reverse cursors. self.has_next = (current_position is not None) or (offset > 0) self.has_previous = has_following_position if self.has_next: self.next_position = current_position if self.has_previous: self.previous_position = following_position else: # Determine next and previous positions for forward cursors. self.has_next = has_following_position self.has_previous = (current_position is not None) or (offset > 0) if self.has_next: self.next_position = following_position 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) and self.template is not None: self.display_page_controls = True return self.page