8000 Add support for pagination in OpenAPI response schemas (#6867) · coderanger/django-rest-framework@f8c1644 · GitHub
[go: up one dir, main page]

Skip to content

Commit f8c1644

Browse files
reupencarltongibson
authored andcommitted
Add support for pagination in OpenAPI response schemas (encode#6867)
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.
1 parent ec1b141 commit f8c1644

File tree

4 files changed

+225
-4
lines changed

4 files changed

+225
-4
lines changed

rest_framework/pagination.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ def paginate_queryset(self, queryset, request, view=None): # pragma: no cover
138138
def get_paginated_response(self, data): # pragma: no cover
139139
raise NotImplementedError('get_paginated_response() must be implemented.')
140140

141+
def get_paginated_response_schema(self, schema):
142+
return schema
143+
141144
def to_html(self): # pragma: no cover
142145
raise NotImplementedError('to_html() must be implemented to display page controls.')
143146

@@ -222,6 +225,26 @@ def get_paginated_response(self, data):
222225
('results', data)
223226
]))
224227

228+
def get_paginated_response_schema(self, schema):
229+
return {
230+
'type': 'object',
231+
'properties': {
232+
'count': {
233+
'type': 'integer',
234+
'example': 123,
235+
},
236+
'next': {
237+
'type': 'string',
238+
'nullable': True,
239+
},
240+
'previous': {
241+
'type': 'string',
242+
'nullable': True,
243+
},
244+
'results': schema,
245+
},
246+
}
247+
225248
def get_page_size(self, request):
226249
if self.page_size_query_param:
227250
try:
@@ -369,6 +392,26 @@ def get_paginated_response(self, data):
369392
('results', data)
370393
]))
371394

395+
def get_paginated_response_schema(self, schema):
396+
return {
397+
'type': 'object',
398+
'properties': {
399+
'count': {
400+
'type': 'integer',
401+
'example': 123,
402+
},
403+
'next': {
404+
'type': 'string',
405+
'nullable': True,
406+
},
407+
'previous': {
408+
'type': 'string',
409+
'nullable': True,
410+
},
411+
'results': schema,
412+
},
413+
}
414+
372415
def get_limit(self, request):
373416
if self.limit_query_param:
374417
try:
@@ -840,6 +883,22 @@ def get_paginated_response(self, data):
840883
('results', data)
841884
]))
842885

886+
def get_paginated_response_schema(self, schema):
887+
return {
888+
'type': 'object',
889+
'properties': {
890+
'next': {
891+
'type': 'string',
892+
'nullable': True,
893+
},
894+
'previous': {
895+
'type': 'string',
896+
'nullable': True,
897+
},
898+
'results': schema,
899+
},
900+
}
901+
843902
def get_html_context(self):
844903
return {
845904
'previous_url': self.get_previous_link(),

rest_framework/schemas/openapi.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,11 +206,10 @@ def _get_pagination_parameters(self, path, method):
206206
if not is_list_view(path, method, view):
207207
return []
208208

209-
pagination = getattr(view, 'pagination_class', None)
210-
if not pagination:
209+
paginator = self._get_pagninator()
210+
if not paginator:
211211
return []
212212

213-
paginator = view.pagination_class()
214213
return paginator.get_schema_operation_parameters(view)
215214

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

425+
def _get_pagninator(self):
426+
pagination_class = getattr(self.view, 'pagination_class', None)
427+
if pagination_cla F438 ss:
428+
return pagination_class()
429+
430+
return None
431+
426432
def _get_serializer(self, method, path):
427433
view = self.view
428434

@@ -489,6 +495,9 @@ def _get_responses(self, path, method):
489495
'type': 'array',
490496
'items': item_schema,
491497
}
498+
paginator = self._get_pagninator()
499+
if paginator:
500+
response_schema = paginator.get_paginated_response_schema(response_schema)
492501
else:
493502
response_schema = item_schema
494503

tests/schemas/test_openapi.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,58 @@ class View(generics.GenericAPIView):
264264
},
265265
}
266266

267+
def test_paginated_list_response_body_generation(self):
268+
"""Test that pagination properties are added for a paginated list view."""
269+
path = '/'
270+
method = 'GET'
271+
272+
class Pagination(pagination.BasePagination):
273+
def get_paginated_response_schema(self, schema):
274+
return {
275+
'type': 'object',
276+
'item': schema,
277+
}
278+
279+
class ItemSerializer(serializers.Serializer):
280+
text = serializers.CharField()
281+
282+
class View(generics.GenericAPIView):
283+
serializer_class = ItemSerializer
284+
pagination_class = Pagination
285+
286+
view = create_view(
287+
View,
288+
method,
289+
create_request(path),
290+
)
291+
inspector = AutoSchema()
292+
inspector.view = view
293+
294+
responses = inspector._get_responses(path, method)
295+
assert responses == {
296+
'200': {
297+
'description': '',
298+
'content': {
299+
'application/json': {
300+
'schema': {
301+
'type': 'object',
302+
'item': {
303+
'type': 'array',
304+
'items': {
305+
'properties': {
306+
'text': {
307+
'type': 'string',
308+
},
309+
},
310+
'required': ['text'],
311+
},
312+
},
313+
},
314+
},
315+
},
316+
},
317+
}
318+
267319
def test_delete_response_body_generation(self):
268320
"""Test that a view's delete method generates a proper response body schema."""
269321
path = '/{id}/'
@@ -288,15 +340,27 @@ class View(generics.DestroyAPIView):
288340
}
289341

290342
def test_retrieve_response_body_generation(self):
291-
"""Test that a list of properties is returned for retrieve item views."""
343+
"""
344+
Test that a list of properties is returned for retrieve item views.
345+
346+
Pagination properties should not be added as the view represents a single item.
347+
"""
292348
path = '/{id}/'
293349
method = 'GET'
294350

351+
class Pagination(pagination.BasePagination):
352+
def get_paginated_response_schema(self, schema):
353+
return {
354+
'type': 'object',
355+
'item': schema,
356+
}
357+
295358
class ItemSerializer(serializers.Serializer):
296359
text = serializers.CharField()
297360

298361
class View(generics.GenericAPIView):
299362
serializer_class = ItemSerializer
363+
pagination_class = Pagination
300364

301365
view = create_view(
302366
View,

tests/test_pagination.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,37 @@ def test_invalid_page(self):
259259
with pytest.raises(exceptions.NotFound):
260260
self.paginate_queryset(request)
261261

262+
def test_get_paginated_response_schema(self):
263+
unpaginated_schema = {
264+
'type': 'object',
265+
'item': {
266+
'properties': {
267+
'test-property': {
268+
'type': 'integer',
269+
},
270+
},
271+
},
272+
}
273+
274+
assert self.pagination.get_paginated_response_schema(unpaginated_schema) == {
275+
'type': 'object',
276+
'properties': {
277+
'count': {
278+
'type': 'integer',
279+
'example': 123,
280+
},
281+
'next': {
282+
'type': 'string',
283+
'nullable': True,
284+
},
285+
'previous': {
286+
'type': 'string',
287+
'nullable': True,
288+
},
289+
'results': unpaginated_schema,
290+
},
291+
}
292+
262293

263294
class TestPageNumberPaginationOverride:
264295
"""
@@ -535,6 +566,37 @@ def test_max_limit(self):
535566
assert content.get('next') == next_url
536567
assert content.get('previous') == prev_url
537568

569+
def test_get_paginated_response_schema(self):
570+
unpaginated_schema = {
571+
'type': 'object',
572+
'item': {
573+
'properties': {
574+
'test-property': {
575+
'type': 'integer',
576+
},
577+
},
578+
},
579+
}
580+
581+
assert self.pagination.get_paginated_response_schema(unpaginated_schema) == {
582+
'type': 'object',
583+
'properties': {
584+
'count': {
585+
'type': 'integer',
586+
'example': 123,
587+
},
588+
'next': {
589+
'type': 'string',
590+
'nullable': True,
591+
},
592+
'previous': {
593+
'type': 'string',
594+
'nullable': True,
595+
},
596+
'results': unpaginated_schema,
597+
},
598+
}
599+
538600

539601
class CursorPaginationTestsMixin:
540602

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

899+
def test_get_paginated_response_schema(self):
900+
unpaginated_schema = {
901+
'type': 'object',
902+
'item': {
903+
'properties': {
904+
'test-property': {
905+
'type': 'integer',
906+
},
907+
},
908+
},
909+
}
910+
911+
assert self.pagination.get_paginated_response_schema(unpaginated_schema) == {
912+
'type': 'object',
913+
'properties': {
914+
'next': {
915+
'type': 'string',
916+
'nullable': True,
917+
},
918+
'previous': {
919+
'type': 'string',
920+
'nullable': True,
921+
},
922+
'results': unpaginated_schema,
923+
},
924+
}
925+
837926

838927
class TestCursorPagination(CursorPaginationTestsMixin):
839928
"""

0 commit comments

Comments
 (0)
0