Skip to content

Commit 7038571

Browse files
kmwenjatomchristie
authored andcommitted
Enable cursor pagination of value querysets. (#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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy