diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index 1d1e09b46a..2d74882ade 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -290,7 +290,8 @@ class MyView(APIView): ### OperationId -The schema generator generates an [operationid](openapi-operationid) for each operation. This `operationId` is deduced from the model name, serializer name or view name. The operationId may looks like "ListItems", "RetrieveItem", "UpdateItem", etc.. +The schema generator generates an [operationid](openapi-operationid) for each operation. This `operationId` is deduced from the model name, serializer name or view name. The operationId may looks like "listItems", "retrieveItem", "updateItem", etc.. +The `operationId` is camelCase by convention. If you have several views with the same model, the generator may generate duplicate operationId. In order to work around this, you can override the second part of the operationId: operation name. @@ -303,7 +304,7 @@ class ExampleView(APIView): schema = AutoSchema(operation_id_base="Custom") ``` -The previous example will generate the following operationId: "ListCustoms", "RetrieveCustom", "UpdateCustom", "PartialUpdateCustom", "DestroyCustom". +The previous example will generate the following operationId: "listCustoms", "retrieveCustom", "updateCustom", "partialUpdateCustom", "destroyCustom". You need to provide the singular form of he operation name. For the list operation, a "s" will be appended at the end of the operation. If you need more configuration over the `operationId` field, you can override the `get_operation_id_base` and `get_operation_id` methods from the `AutoSchema` class: diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 6bed120922..1d0ec35d54 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -131,11 +131,11 @@ def __init__(self, tags=None, operation_id_base=None, component_name=None): response_media_types = [] method_mapping = { - 'get': 'Retrieve', - 'post': 'Create', - 'put': 'Update', - 'patch': 'PartialUpdate', - 'delete': 'Destroy', + 'get': 'retrieve', + 'post': 'create', + 'put': 'update', + 'patch': 'partialUpdate', + 'delete': 'destroy', } def get_operation(self, path, method): @@ -195,6 +195,12 @@ def get_components(self, path, method): content = self._map_serializer(serializer) return {component_name: content} + def _to_camel_case(self, snake_str): + components = snake_str.split('_') + # We capitalize the first letter of each component except the first one + # with the 'title' method and join them together. + return components[0] + ''.join(x.title() for x in components[1:]) + def get_operation_id_base(self, path, method, action): """ Compute the base part for operation ID from the model, serializer or view name. @@ -240,7 +246,7 @@ def get_operation_id(self, path, method): if is_list_view(path, method, self.view): action = 'list' elif method_name not in self.method_mapping: - action = method_name + action = self._to_camel_case(method_name) else: action = self.method_mapping[method.lower()] diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 35d676d6c3..c9f6d967ec 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -158,7 +158,7 @@ def test_path_with_id_parameter(self): operation = inspector.get_operation(path, method) assert operation == { - 'operationId': 'RetrieveDocStringExampleDetail', + 'operationId': 'retrieveDocStringExampleDetail', 'description': 'A description of my GET operation.', 'parameters': [{ 'description': '', @@ -735,6 +735,23 @@ def test_duplicate_operation_id(self): print(str(w[-1].message)) assert 'You have a duplicated operationId' in str(w[-1].message) + def test_operation_id_viewset(self): + router = routers.SimpleRouter() + router.register('account', views.ExampleViewSet, basename="account") + urlpatterns = router.urls + + generator = SchemaGenerator(patterns=urlpatterns) + + request = create_request('/') + schema = generator.get_schema(request=request) + print(schema) + assert schema['paths']['/account/']['get']['operationId'] == 'listExampleViewSets' + assert schema['paths']['/account/']['post']['operationId'] == 'createExampleViewSet' + assert schema['paths']['/account/{id}/']['get']['operationId'] == 'retrieveExampleViewSet' + assert schema['paths']['/account/{id}/']['put']['operationId'] == 'updateExampleViewSet' + assert schema['paths']['/account/{id}/']['patch']['operationId'] == 'partialUpdateExampleViewSet' + assert schema['paths']['/account/{id}/']['delete']['operationId'] == 'destroyExampleViewSet' + def test_serializer_datefield(self): path = '/' method = 'GET' diff --git a/tests/schemas/views.py b/tests/schemas/views.py index 1c8235b425..5645f59bf5 100644 --- a/tests/schemas/views.py +++ b/tests/schemas/views.py @@ -11,7 +11,7 @@ from rest_framework.response import Response from rest_framework.schemas.openapi import AutoSchema from rest_framework.views import APIView -from rest_framework.viewsets import GenericViewSet +from rest_framework.viewsets import GenericViewSet, ViewSet class ExampleListView(APIView): @@ -215,3 +215,25 @@ def get(self, *args, **kwargs): serializer = self.get_serializer(data=now.date(), datetime=now) return Response(serializer.data) + + +class ExampleViewSet(ViewSet): + serializer_class = ExampleSerializerModel + + def list(self, request): + pass + + def create(self, request): + pass + + def retrieve(self, request, pk=None): + pass + + def update(self, request, pk=None): + pass + + def partial_update(self, request, pk=None): + pass + + def destroy(self, request, pk=None): + pass