mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-10 19:56:59 +03:00
Add support for pagination in OpenAPI response schemas (#6867)
Refs #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.
This commit is contained in:
parent
ec1b14174f
commit
f8c16441fa
|
@ -138,6 +138,9 @@ class BasePagination:
|
|||
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.')
|
||||
|
||||
|
@ -222,6 +225,26 @@ class PageNumberPagination(BasePagination):
|
|||
('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:
|
||||
|
@ -369,6 +392,26 @@ class LimitOffsetPagination(BasePagination):
|
|||
('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:
|
||||
|
@ -840,6 +883,22 @@ class CursorPagination(BasePagination):
|
|||
('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(),
|
||||
|
|
|
@ -206,11 +206,10 @@ class AutoSchema(ViewInspector):
|
|||
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):
|
||||
|
@ -423,6 +422,13 @@ class AutoSchema(ViewInspector):
|
|||
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
|
||||
|
||||
|
@ -489,6 +495,9 @@ class AutoSchema(ViewInspector):
|
|||
'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
|
||||
|
||||
|
|
|
@ -264,6 +264,58 @@ class TestOperationIntrospection(TestCase):
|
|||
},
|
||||
}
|
||||
|
||||
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}/'
|
||||
|
@ -288,15 +340,27 @@ class TestOperationIntrospection(TestCase):
|
|||
}
|
||||
|
||||
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,
|
||||
|
|
|
@ -259,6 +259,37 @@ class TestPageNumberPagination:
|
|||
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:
|
||||
"""
|
||||
|
@ -535,6 +566,37 @@ class TestLimitOffset:
|
|||
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:
|
||||
|
||||
|
@ -834,6 +896,33 @@ class CursorPaginationTestsMixin:
|
|||
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):
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue
Block a user