Skip to content

Commit 7038571

Browse files
kmwenjatomchristie
authored andcommitted
Enable cursor pagination of value querysets. (encode#4569)
To do `GROUP_BY` queries in django requires one to use `.values()` eg this groups posts by user getting a count of posts per user. ``` Posts.objects.order_by('user').values('user').annotate(post_count=Count('post')) ``` This would produce a value queryset which serializes its result objects as dictionaries while `CursorPagination` requires a queryset with result objects that are model instances. This commit enables cursor pagination for value querysets. - had to mangle the tests a bit to test it out. They might need some refactoring. - tried the same for `.values_list()` but it turned out to be trickier than I expected since you have to use tuple indexes.
1 parent 97d8484 commit 7038571

File tree

2 files changed

+147
-80
lines changed

2 files changed

+147
-80
lines changed

rest_framework/pagination.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,11 @@ def encode_cursor(self, cursor):
711711
return replace_query_param(self.base_url, self.cursor_query_param, encoded)
712712

713713
def _get_position_from_instance(self, instance, ordering):
714-
attr = getattr(instance, ordering[0].lstrip('-'))
714+
field_name = ordering[0].lstrip('-')
715+
if isinstance(instance, dict):
716+
attr = instance[field_name]
717+
else:
718+
attr = getattr(instance, field_name)
715719
return six.text_type(attr)
716720

717721
def get_paginated_response(self, data):

tests/test_pagination.py

Lines changed: 142 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import pytest
55
from django.core.paginator import Paginator as DjangoPaginator
6+
from django.db import models
7+
from django.test import TestCase
68

79
from rest_framework import (
810
exceptions, filters, generics, pagination, serializers, status
@@ -530,85 +532,7 @@ def test_max_limit(self):
530532
assert content.get('previous') == prev_url
531533

532534

533-
class TestCursorPagination:
534-
"""
535-
Unit tests for `pagination.CursorPagination`.
536-
"""
537-
538-
def setup(self):
539-
class MockObject(object):
540-
def __init__(self, idx):
541-
self.created = idx
542-
543-
class MockQuerySet(object):
544-
def __init__(self, items):
545-
self.items = items
546-
547-
def filter(self, created__gt=None, created__lt=None):
548-
if created__gt is not None:
549-
return MockQuerySet([
550-
item for item in self.items
551-
if item.created > int(created__gt)
552-
])
553-
554-
assert created__lt is not None
555-
return MockQuerySet([
556-
item for item in self.items
557-
if item.created < int(created__lt)
558-
])
559-
560-
def order_by(self, *ordering):
561-
if ordering[0].startswith('-'):
562-
return MockQuerySet(list(reversed(self.items)))
563-
return self
564-
565-
def __getitem__(self, sliced):
566-
return self.items[sliced]
567-
568-
class ExamplePagination(pagination.CursorPagination):
569-
page_size = 5
570-
ordering = 'created'
571-
572-
self.pagination = ExamplePagination()
573-
self.queryset = MockQuerySet([
574-
MockObject(idx) for idx in [
575-
1, 1, 1, 1, 1,
576-
1, 2, 3, 4, 4,
577-
4, 4, 5, 6, 7,
578-
7, 7, 7, 7, 7,
579-
7, 7, 7, 8, 9,
580-
9, 9, 9, 9, 9
581-
]
582-
])
583-
584-
def get_pages(self, url):
585-
"""
586-
Given a URL return a tuple of:
587-
588-
(previous page, current page, next page, previous url, next url)
589-
"""
590-
request = Request(factory.get(url))
591-
queryset = self.pagination.paginate_queryset(self.queryset, request)
592-
current = [item.created for item in queryset]
593-
594-
next_url = self.pagination.get_next_link()
595-
previous_url = self.pagination.get_previous_link()
596-
597-
if next_url is not None:
598-
request = Request(factory.get(next_url))
599-
queryset = self.pagination.paginate_queryset(self.queryset, request)
600-
next = [item.created for item in queryset]
601-
else:
602-
next = None
603-
604-
if previous_url is not None:
605-
request = Request(factory.get(previous_url))
606-
queryset = self.pagination.paginate_queryset(self.queryset, request)
607-
previous = [item.created for item in queryset]
608-
else:
609-
previous = None
610-
611-
return (previous, current, next, previous_url, next_url)
535+
class CursorPaginationTestsMixin:
612536

613537
def test_invalid_cursor(self):
614538
request = Request(factory.get('/', {'cursor': '123'}))
@@ -703,6 +627,145 @@ def test_cursor_pagination(self):
703627
assert isinstance(self.pagination.to_html(), type(''))
704628

705629

630+
class TestCursorPagination(CursorPaginationTestsMixin):
631+
"""
632+
Unit tests for `pagination.CursorPagination`.
633+
"""
634+
635+
def setup(self):
636+
class MockObject(object):
637+
def __init__(self, idx):
638+
self.created = idx
639+
640+
class MockQuerySet(object):
641+
def __init__(self, items):
642+
self.items = items
643+
644+
def filter(self, created__gt=None, created__lt=None):
645+
if created__gt is not None:
646+
return MockQuerySet([
647+
item for item in self.items
648+
if item.created > int(created__gt)
649+
])
650+
651+
assert created__lt is not None
652+
return MockQuerySet([
653+
item for item in self.items
654+
if item.created < int(created__lt)
655+
])
656+
657+
def order_by(self, *ordering):
658+
if ordering[0].startswith('-'):
659+
return MockQuerySet(list(reversed(self.items)))
660+
return self
661+
662+
def __getitem__(self, sliced):
663+
return self.items[sliced]
664+
665+
class ExamplePagination(pagination.CursorPagination):
666+
page_size = 5
667+
ordering = 'created'
668+
669+
self.pagination = ExamplePagination()
670+
self.queryset = MockQuerySet([
671+
MockObject(idx) for idx in [
672+
1, 1, 1, 1, 1,
673+
1, 2, 3, 4, 4,
674+
4, 4, 5, 6, 7,
675+
7, 7, 7, 7, 7,
676+
7, 7, 7, 8, 9,
677+
9, 9, 9, 9, 9
678+
]
679+
])
680+
681+
def get_pages(self, url):
682+
"""
683+
Given a URL return a tuple of:
684+
685+
(previous page, current page, next page, previous url, next url)
686+
"""
687+
request = Request(factory.get(url))
688+
queryset = self.pagination.paginate_queryset(self.queryset, request)
689+
current = [item.created for item in queryset]
690+
691+
next_url = self.pagination.get_next_link()
692+
previous_url = self.pagination.get_previous_link()
693+
694+
if next_url is not None:
695+
request = Request(factory.get(next_url))
696+
queryset = self.pagination.paginate_queryset(self.queryset, request)
697+
next = [item.created for item in queryset]
698+
else:
699+
next = None
700+
701+
if previous_url is not None:
702+
request = Request(factory.get(previous_url))
703+
queryset = self.pagination.paginate_queryset(self.queryset, request)
704+
previous = [item.created for item in queryset]
705+
else:
706+
previous = None
707+
708+
return (previous, current, next, previous_url, next_url)
709+
710+
711+
class CursorPaginationModel(models.Model):
712+
created = models.IntegerField()
713+
714+
715+
class TestCursorPaginationWithValueQueryset(CursorPaginationTestsMixin, TestCase):
716+
"""
717+
Unit tests for `pagination.CursorPagination` for value querysets.
718+
"""
719+
720+
def setUp(self):
721+
class ExamplePagination(pagination.CursorPagination):
722+
page_size = 5
723+
ordering = 'created'
724+
725+
self.pagination = ExamplePagination()
726+
data = [
727+
1, 1, 1, 1, 1,
728+
1, 2, 3, 4, 4,
729+
4, 4, 5, 6, 7,
730+
7, 7, 7, 7, 7,
731+
7, 7, 7, 8, 9,
732+
9, 9, 9, 9, 9
733+
]
734+
for idx in data:
735+
CursorPaginationModel.objects.create(created=idx)
736+
737+
self.queryset = CursorPaginationModel.objects.values()
738+
739+
def get_pages(self, url):
740+
"""
741+
Given a URL return a tuple of:
742+
743+
(previous page, current page, next page, previous url, next url)
744+
"""
745+
request = Request(factory.get(url))
746+
queryset = self.pagination.paginate_queryset(self.queryset, request)
747+
current = [item['created'] for item in queryset]
748+
749+
next_url = self.pagination.get_next_link()
750+
previous_url = self.pagination.get_previous_link()
751+
752+
if next_url is not None:
753+
request = Request(factory.get(next_url))
754+
queryset = self.pagination.paginate_queryset(self.queryset, request)
755+
next = [item['created'] for item in queryset]
756+
else:
757+
next = None
758+
759+
if previous_url is not None:
760+
request = Request(factory.get(previous_url))
761+
queryset = self.pagination.paginate_queryset(self.queryset, request)
762+
previous = [item['created'] for item in queryset]
763+
else:
764+
previous = None
765+
766+
return (previous, current, next, previous_url, next_url)
767+
768+
706769
def test_get_displayed_page_numbers():
707770
"""
708771
Test our contextual page display function.

0 commit comments

Comments
 (0)