Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for pagination in OpenAPI response schemas #6867

Merged
merged 1 commit into from Sep 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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