Skip to content

Commit

Permalink
Add support for pagination in OpenAPI response schemas (encode#6867)
Browse files Browse the repository at this point in the history
Refs encode#6846

This provides a way for pagination classes to add pagination properties (`count`, `next`, `results` etc.) to OpenAPI response schemas.

A new method `get_paginated_response_schema()` has been added to `BasePagination`. This method is intended to mirror `get_paginated_response()` (which takes a `list` and wraps it in a `dict`). 

Hence, `get_paginated_response_schema()` takes an unpaginated response schema (of type `array`) and wraps that with a schema object of type `object` containing the relevant properties that the pagination class adds to responses.

The default implementation of `BasePagination.get_paginated_response_schema()` simply passes the schema through unmodified, for backwards compatibility.
  • Loading branch information
reupen authored and Pierre Chiquet committed Mar 24, 2020
1 parent b8011d1 commit eef4793
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 4 deletions.
59 changes: 59 additions & 0 deletions rest_framework/pagination.py
Expand Up @@ -138,6 +138,9 @@ def paginate_queryset(self, queryset, request, view=None): # pragma: no cover
def get_paginated_response(self, data): # pragma: no cover
raise NotImplementedError('get_paginated_response() must be implemented.')

def get_paginated_response_schema(self, schema):
return schema

def to_html(self): # pragma: no cover
raise NotImplementedError('to_html() must be implemented to display page controls.')

Expand Down Expand Up @@ -222,6 +225,26 @@ def get_paginated_response(self, data):
('results', data)
]))

def get_paginated_response_schema(self, schema):
return {
'type': 'object',
'properties': {
'count': {
'type': 'integer',
'example': 123,
},
'next': {
'type': 'string',
'nullable': True,
},
'previous': {
'type': 'string',
'nullable': True,
},
'results': schema,
},
}

def get_page_size(self, request):
if self.page_size_query_param:
try:
Expand Down Expand Up @@ -369,6 +392,26 @@ def get_paginated_response(self, data):
('results', data)
]))

def get_paginated_response_schema(self, schema):
return {
'type': 'object',
'properties': {
'count': {
'type': 'integer',
'example': 123,
},
'next': {
'type': 'string',
'nullable': True,
},
'previous': {
'type': 'string',
'nullable': True,
},
'results': schema,
},
}

def get_limit(self, request):
if self.limit_query_param:
try:
Expand Down Expand Up @@ -840,6 +883,22 @@ def get_paginated_response(self, data):
('results', data)
]))

def get_paginated_response_schema(self, schema):
return {
'type': 'object',
'properties': {
'next': {
'type': 'string',
'nullable': True,
},
'previous': {
'type': 'string',
'nullable': True,
},
'results': schema,
},
}

def get_html_context(self):
return {
'previous_url': self.get_previous_link(),
Expand Down
15 changes: 12 additions & 3 deletions rest_framework/schemas/openapi.py
Expand Up @@ -206,11 +206,10 @@ def _get_pagination_parameters(self, path, method):
if not is_list_view(path, method, view):
return []

pagination = getattr(view, 'pagination_class', None)
if not pagination:
paginator = self._get_pagninator()
if not paginator:
return []

paginator = view.pagination_class()
return paginator.get_schema_operation_parameters(view)

def _map_field(self, field):
Expand Down Expand Up @@ -423,6 +422,13 @@ def _map_field_validators(self, validators, schema):
schema['maximum'] = int(digits * '9') + 1
schema['minimum'] = -schema['maximum']

def _get_pagninator(self):
pagination_class = getattr(self.view, 'pagination_class', None)
if pagination_class:
return pagination_class()

return None

def _get_serializer(self, method, path):
view = self.view

Expand Down Expand Up @@ -489,6 +495,9 @@ def _get_responses(self, path, method):
'type': 'array',
'items': item_schema,
}
paginator = self._get_pagninator()
if paginator:
response_schema = paginator.get_paginated_response_schema(response_schema)
else:
response_schema = item_schema

Expand Down
66 changes: 65 additions & 1 deletion tests/schemas/test_openapi.py
Expand Up @@ -264,6 +264,58 @@ class View(generics.GenericAPIView):
},
}

def test_paginated_list_response_body_generation(self):
"""Test that pagination properties are added for a paginated list view."""
path = '/'
method = 'GET'

class Pagination(pagination.BasePagination):
def get_paginated_response_schema(self, schema):
return {
'type': 'object',
'item': schema,
}

class ItemSerializer(serializers.Serializer):
text = serializers.CharField()

class View(generics.GenericAPIView):
serializer_class = ItemSerializer
pagination_class = Pagination

view = create_view(
View,
method,
create_request(path),
)
inspector = AutoSchema()
inspector.view = view

responses = inspector._get_responses(path, method)
assert responses == {
'200': {
'description': '',
'content': {
'application/json': {
'schema': {
'type': 'object',
'item': {
'type': 'array',
'items': {
'properties': {
'text': {
'type': 'string',
},
},
'required': ['text'],
},
},
},
},
},
},
}

def test_delete_response_body_generation(self):
"""Test that a view's delete method generates a proper response body schema."""
path = '/{id}/'
Expand All @@ -288,15 +340,27 @@ class View(generics.DestroyAPIView):
}

def test_retrieve_response_body_generation(self):
"""Test that a list of properties is returned for retrieve item views."""
"""
Test that a list of properties is returned for retrieve item views.
Pagination properties should not be added as the view represents a single item.
"""
path = '/{id}/'
method = 'GET'

class Pagination(pagination.BasePagination):
def get_paginated_response_schema(self, schema):
return {
'type': 'object',
'item': schema,
}

class ItemSerializer(serializers.Serializer):
text = serializers.CharField()

class View(generics.GenericAPIView):
serializer_class = ItemSerializer
pagination_class = Pagination

view = create_view(
View,
Expand Down
89 changes: 89 additions & 0 deletions tests/test_pagination.py
Expand Up @@ -259,6 +259,37 @@ def test_invalid_page(self):
with pytest.raises(exceptions.NotFound):
self.paginate_queryset(request)

def test_get_paginated_response_schema(self):
unpaginated_schema = {
'type': 'object',
'item': {
'properties': {
'test-property': {
'type': 'integer',
},
},
},
}

assert self.pagination.get_paginated_response_schema(unpaginated_schema) == {
'type': 'object',
'properties': {
'count': {
'type': 'integer',
'example': 123,
},
'next': {
'type': 'string',
'nullable': True,
},
'previous': {
'type': 'string',
'nullable': True,
},
'results': unpaginated_schema,
},
}


class TestPageNumberPaginationOverride:
"""
Expand Down Expand Up @@ -535,6 +566,37 @@ def test_max_limit(self):
assert content.get('next') == next_url
assert content.get('previous') == prev_url

def test_get_paginated_response_schema(self):
unpaginated_schema = {
'type': 'object',
'item': {
'properties': {
'test-property': {
'type': 'integer',
},
},
},
}

assert self.pagination.get_paginated_response_schema(unpaginated_schema) == {
'type': 'object',
'properties': {
'count': {
'type': 'integer',
'example': 123,
},
'next': {
'type': 'string',
'nullable': True,
},
'previous': {
'type': 'string',
'nullable': True,
},
'results': unpaginated_schema,
},
}


class CursorPaginationTestsMixin:

Expand Down Expand Up @@ -834,6 +896,33 @@ def test_cursor_pagination_with_page_size_negative(self):
assert current == [1, 1, 1, 1, 1]
assert next == [1, 2, 3, 4, 4]

def test_get_paginated_response_schema(self):
unpaginated_schema = {
'type': 'object',
'item': {
'properties': {
'test-property': {
'type': 'integer',
},
},
},
}

assert self.pagination.get_paginated_response_schema(unpaginated_schema) == {
'type': 'object',
'properties': {
'next': {
'type': 'string',
'nullable': True,
},
'previous': {
'type': 'string',
'nullable': True,
},
'results': unpaginated_schema,
},
}


class TestCursorPagination(CursorPaginationTestsMixin):
"""
Expand Down

0 comments on commit eef4793

Please sign in to comment.