Understanding django drf's PageNumberPagination

55 阅读2分钟

Pagination is only performed automatically if you're using the generic views or viewsets. If you're using a regular APIView, you'll need to call into the pagination API yourself to ensure you return a paginated response

We need pagination of course. Let have a try.

Setup (also is GLOBAL level)

First we add django system config:

REST_FRAMEWORK = {
+    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
+    'PAGE_SIZE': 5
}

image.png

ViewSet level

Now it works on all list endpoints. But how to specify some endpoint's page size?

The official document tell us to define a class implementate Pagination class:

class LittleResultsSetPagination(PageNumberPagination):
    page_size = 2
    max_page_size = 10


class UserViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, UserCanOnlyModifyHimSelfPermission]
    pagination_class = LittleResultsSetPagination

Maybe almost every list endpoint need pagination. Only one want return all data. How to implementate?

class UserViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, UserCanOnlyModifyHimSelfPermission]
    pagination_class = None # Intuitive 😂

Let user specify(from url query params)

Now we want users to be able to specify the page size they need but I didn't find it on document. Let find it out from debug the source code.

Because we are using ListModelMixin. So we add a break point at list:

image.png

Meaningful method paginate_queryset. I like it.

image.png

All the pagination works on this paginator instance.

image.png

image.png

It will use request.query_params[self.page_size_query_param] to do paginate.Means we need to set this value with user's query_params.

class LittleResultsSetPagination(PageNumberPagination):
    page_size_query_param = 'page_size'


class UserViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, UserCanOnlyModifyHimSelfPermission]
    pagination_class = LittleResultsSetPagination

It works!

PS: PageNumberPagination also have a lot of attributes we could override. Read the doc!

Custom response field name

Frontend may want response data looks like code below but official doesn't support any docuemnt about this. Let find it out by ourselves.

{
-    "count": 9,
+    "total": 9,
-    "next": "http://127.0.0.1:22222/user/?page=2",
+    "have_next": true,
-    "previous": null,
+    "have_previous": false,
-    "results": [
+    "list": [
        {
            "id": 14
        }
    ]
}

image.png

😂 Cannot config! Hard code dict key!

But this method is very short and easy. So we can override it(even can add some other fields):

class LittleResultsSetPagination(PageNumberPagination):
    page_size_query_param = 'page_size'

    def get_paginated_response(self, data):
        from collections import OrderedDict
        return Response(OrderedDict([
            ('total', self.page.paginator.count),
            ('have_next', bool(self.get_next_link())),
            ('have_previous', bool(self.get_previous_link())),
            ('list', data)
        ]))

At last, you can set this class as django's default, global level PagationClass:

REST_FRAMEWORK = {
-    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
+   'DEFAULT_PAGINATION_CLASS': 'your_class_foler.LittleResultsSetPagination',
    'PAGE_SIZE': 5
}

Test case

class MyResultsSetPagination(PageNumberPagination):
    page_size_query_param = 'pageSize'
    page_query_param = 'current'
    page_size = 10

    def get_paginated_response(self, data):
        from collections import OrderedDict
        return Response(OrderedDict([
            ('pageSize', self.get_page_size(self.request)),
            ('total', self.page.paginator.count),
            ('current', self.page.number),
            ('haveNext', bool(self.get_next_link())),
            ('havePrevious', bool(self.get_previous_link())),
            ('list', data)
        ]))
        

class TestMyResultsSetPagination(TestCase, TransactionTestCase):
    databases = {'default'}

    def setUp(self):
        from django.core.management import call_command
        call_command('migrate', verbosity=0, interactive=False, database='default')
        users = [User(username=f'pagination_user_{i}') for i in range(17)]
        User.objects.bulk_create(users)

    def test_get_paginated_response_without_specify_query_params(self):
        response = self.client.get('/users/')
        self.assertEqual(response.data['total'], User.objects.count())
        self.assertEqual(response.data['current'], 1)
        self.assertEqual(response.data['pageSize'], MyResultsSetPagination.page_size)
        self.assertEqual(len(response.data['list']), MyResultsSetPagination.page_size)

    def test_get_paginated_response_with_specify_query_params(self):
        response = self.client.get('/users/?pageSize=12')
        self.assertEqual(response.data['total'], User.objects.count())
        self.assertEqual(response.data['current'], 1)
        self.assertEqual(response.data['pageSize'], 12)
        self.assertEqual(len(response.data['list']), 12)

        response = self.client.get('/users/?pageSize=12&current=2')
        self.assertEqual(response.data['total'], User.objects.count())
        self.assertEqual(response.data['current'], 2)
        self.assertEqual(response.data['pageSize'], 12)
        self.assertEqual(len(response.data['list']), 5)

Thanks for reading!