From 132f2ea9e2416f08bae617a0fa5c21c0473d232b Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 14 Jan 2022 15:41:12 +0300 Subject: [PATCH 01/22] Init --- cvat/apps/engine/models.py | 4 + cvat/apps/engine/urls.py | 79 +-- cvat/apps/engine/views.py | 897 ++++++++++++++++++++++---------- cvat/apps/iam/urls.py | 2 +- cvat/apps/iam/views.py | 28 +- cvat/apps/restrictions/views.py | 10 +- cvat/apps/training/views.py | 8 +- cvat/requirements/base.txt | 2 +- cvat/settings/base.py | 17 +- 9 files changed, 704 insertions(+), 343 deletions(-) diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index c20fc8782dd..0198ffbcf3e 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -44,6 +44,10 @@ class StatusChoice(str, Enum): def choices(cls): return tuple((x.value, x.name) for x in cls) + @classmethod + def list(cls): + return list(map(lambda x: x.value, cls)) + def __str__(self): return self.value diff --git a/cvat/apps/engine/urls.py b/cvat/apps/engine/urls.py index 1fd820ac147..225dd9534df 100644 --- a/cvat/apps/engine/urls.py +++ b/cvat/apps/engine/urls.py @@ -7,54 +7,55 @@ from . import views from rest_framework import routers from rest_framework import permissions -from drf_yasg.views import get_schema_view -from drf_yasg import openapi + from django.views.generic import RedirectView from django.conf import settings from cvat.apps.restrictions.views import RestrictionsViewSet from cvat.apps.iam.decorators import login_required from cvat.apps.training.views import PredictView -schema_view = get_schema_view( - openapi.Info( - title="CVAT REST API", - default_version='v1', - description="REST API for Computer Vision Annotation Tool (CVAT)", - terms_of_service="https://www.google.com/policies/terms/", - contact=openapi.Contact(email="nikita.manovich@intel.com"), - license=openapi.License(name="MIT License"), - ), - public=True, - permission_classes=(permissions.IsAuthenticated,), -) +from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView + +# schema_view = get_schema_view( +# openapi.Info( +# title="CVAT REST API", +# default_version='v1', +# description="REST API for Computer Vision Annotation Tool (CVAT)", +# terms_of_service="https://www.google.com/policies/terms/", +# contact=openapi.Contact(email="nikita.manovich@intel.com"), +# license=openapi.License(name="MIT License"), +# ), +# public=True, +# permission_classes=(permissions.IsAuthenticated,), +# ) # drf-yasg component doesn't handle correctly URL_FORMAT_OVERRIDE and # send requests with ?format=openapi suffix instead of ?scheme=openapi. # We map the required parameter explicitly and add it into query arguments # on the server side. -def wrap_swagger(view): - @login_required - def _map_format_to_schema(request, scheme=None): - if 'format' in request.GET: - request.GET = request.GET.copy() - format_alias = settings.REST_FRAMEWORK['URL_FORMAT_OVERRIDE'] - request.GET[format_alias] = request.GET['format'] +# def wrap_swagger(view): +# @login_required +# def _map_format_to_schema(request, scheme=None): +# if 'format' in request.GET: +# request.GET = request.GET.copy() +# format_alias = settings.REST_FRAMEWORK['URL_FORMAT_OVERRIDE'] +# request.GET[format_alias] = request.GET['format'] - return view(request, format=scheme) +# return view(request, format=scheme) - return _map_format_to_schema +# return _map_format_to_schema router = routers.DefaultRouter(trailing_slash=False) -router.register('projects', views.ProjectViewSet) -router.register('tasks', views.TaskViewSet) -router.register('jobs', views.JobViewSet) -router.register('users', views.UserViewSet) +router.register('projects', views.ProjectViewSet, basename='projects') +router.register('tasks', views.TaskViewSet, basename='tasks') +router.register('jobs', views.JobViewSet, basename='jobs') +router.register('users', views.UserViewSet, basename='users') router.register('server', views.ServerViewSet, basename='server') -router.register('issues', views.IssueViewSet) -router.register('comments', views.CommentViewSet) +router.register('issues', views.IssueViewSet, basename='issues') +router.register('comments', views.CommentViewSet, basename='comments') router.register('restrictions', RestrictionsViewSet, basename='restrictions') router.register('predict', PredictView, basename='predict') -router.register('cloudstorages', views.CloudStorageViewSet) +router.register('cloudstorages', views.CloudStorageViewSet, basename='cloudstorages') urlpatterns = [ # Entry point for a client @@ -62,15 +63,19 @@ def _map_format_to_schema(request, scheme=None): query_string=True)), # documentation for API - path('api/swagger', wrap_swagger( - schema_view.without_ui(cache_timeout=0)), name='schema-json'), - path('api/swagger/', wrap_swagger( - schema_view.with_ui('swagger', cache_timeout=0)), name='schema-swagger-ui'), - path('api/docs/', wrap_swagger( - schema_view.with_ui('redoc', cache_timeout=0)), name='schema-redoc'), + # path('api/swagger', wrap_swagger( + # schema_view.without_ui(cache_timeout=0)), name='schema-json'), + # path('api/swagger/', wrap_swagger( + # schema_view.with_ui('swagger', cache_timeout=0)), name='schema-swagger-ui'), + # path('api/docs/', wrap_swagger( + # schema_view.with_ui('redoc', cache_timeout=0)), name='schema-redoc'), + + path('api/schema/', SpectacularAPIView.as_view(api_version='v1'), name='schema'), + path('api/swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger'), + path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), # entry point for API path('api/v1/', include('cvat.apps.iam.urls')), path('api/v1/', include('cvat.apps.organizations.urls')), - path('api/v1/', include(router.urls)), + path('api/v1/', include((router.urls, 'cvat'), namespace='v1')), ] diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index b9fb09877ca..0fda9d67877 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -25,9 +25,13 @@ from django.utils.decorators import method_decorator from django_filters import rest_framework as filters from django_filters.rest_framework import DjangoFilterBackend -from drf_yasg import openapi -from drf_yasg.inspectors import CoreAPICompatInspector, NotHandled, FieldInspector -from drf_yasg.utils import swagger_auto_schema + +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import ( + OpenApiExample, OpenApiParameter, OpenApiResponse, extend_schema, +) + + from rest_framework import mixins, serializers, status, viewsets from rest_framework.decorators import action from rest_framework.exceptions import APIException, NotFound, ValidationError @@ -80,8 +84,14 @@ def get_serializer(self, *args, **kwargs): pass @staticmethod - @swagger_auto_schema(method='get', operation_summary='Method provides basic CVAT information', - responses={'200': AboutSerializer}) + @extend_schema( + summary='Method provides basic CVAT information', + responses={ + '200': AboutSerializer + }, + tags=['server'], + versions=['v1'], + ) @action(detail=False, methods=['GET'], serializer_class=AboutSerializer) def about(request): from cvat import __version__ as cvat_version @@ -101,7 +111,11 @@ def about(request): return Response(data=serializer.data) @staticmethod - @swagger_auto_schema(method='post', request_body=ExceptionSerializer) + @extend_schema( + request=ExceptionSerializer, + tags=['server'], + versions=['v1'], + ) @action(detail=False, methods=['POST'], serializer_class=ExceptionSerializer) def exception(request): """ @@ -127,7 +141,12 @@ def exception(request): return Response(serializer.data, status=status.HTTP_201_CREATED) @staticmethod - @swagger_auto_schema(method='post', request_body=LogEventSerializer(many=True)) + @extend_schema( + methods=['POST'], + request=LogEventSerializer(many=True), + tags=['server'], + versions=['v1'], + ) @action(detail=False, methods=['POST'], serializer_class=LogEventSerializer) def logs(request): """ @@ -150,10 +169,22 @@ def logs(request): return Response(serializer.data, status=status.HTTP_201_CREATED) @staticmethod - @swagger_auto_schema( - method='get', operation_summary='Returns all files and folders that are on the server along specified path', - manual_parameters=[openapi.Parameter('directory', openapi.IN_QUERY, type=openapi.TYPE_STRING, description='Directory to browse')], - responses={'200' : FileInfoSerializer(many=True)} + @extend_schema( + methods=['GET'], + summary='Returns all files and folders that are on the server along specified path', + parameters=[ + OpenApiParameter( + 'directory', + description='Directory to browse', + location=OpenApiParameter.QUERY, + type=OpenApiTypes.STR, + ) + ], + responses={ + '200' : FileInfoSerializer(many=True) + }, + tags=['server'], + versions=['v1'], ) @action(detail=False, methods=['GET'], serializer_class=FileInfoSerializer) def share(request): @@ -183,16 +214,30 @@ def share(request): status=status.HTTP_400_BAD_REQUEST) @staticmethod - @swagger_auto_schema(method='get', operation_summary='Method provides the list of supported annotations formats', - responses={'200': DatasetFormatsSerializer()}) + @extend_schema( + methods=['GET'], + summary='Method provides the list of supported annotations formats', + responses={ + '200': DatasetFormatsSerializer(), + }, + tags=['server'], + versions=['v1'], + ) @action(detail=False, methods=['GET'], url_path='annotation/formats') def annotation_formats(request): data = dm.views.get_all_formats() return Response(DatasetFormatsSerializer(data).data) @staticmethod - @swagger_auto_schema(method='get', operation_summary='Method provides allowed plugins.', - responses={'200': PluginsSerializer()}) + @extend_schema( + methods=['GET'], + summary='Method provides allowed plugins', + responses={ + '200': PluginsSerializer(), + }, + tags=['server'], + versions=['v1'], + ) @action(detail=False, methods=['GET'], url_path='plugins', serializer_class=PluginsSerializer) def plugins(request): response = { @@ -218,23 +263,84 @@ class Meta: model = models.Project fields = ("id", "name", "owner", "status") -@method_decorator(name='list', decorator=swagger_auto_schema( - operation_summary='Returns a paginated list of projects according to query parameters (12 projects per page)', - manual_parameters=[ - openapi.Parameter('id', openapi.IN_QUERY, description="A unique number value identifying this project", - type=openapi.TYPE_NUMBER), - openapi.Parameter('name', openapi.IN_QUERY, description="Find all projects where name contains a parameter value", - type=openapi.TYPE_STRING), - openapi.Parameter('owner', openapi.IN_QUERY, description="Find all project where owner name contains a parameter value", - type=openapi.TYPE_STRING), - openapi.Parameter('status', openapi.IN_QUERY, description="Find all projects with a specific status", - type=openapi.TYPE_STRING, enum=[str(i) for i in StatusChoice]), - openapi.Parameter('names_only', openapi.IN_QUERY, description="Returns only names and id's of projects.", - type=openapi.TYPE_BOOLEAN)])) -@method_decorator(name='create', decorator=swagger_auto_schema(operation_summary='Method creates a new project')) -@method_decorator(name='retrieve', decorator=swagger_auto_schema(operation_summary='Method returns details of a specific project')) -@method_decorator(name='destroy', decorator=swagger_auto_schema(operation_summary='Method deletes a specific project')) -@method_decorator(name='partial_update', decorator=swagger_auto_schema(operation_summary='Methods does a partial update of chosen fields in a project')) +@method_decorator(name='list', decorator=extend_schema( + summary='Returns a paginated list of projects according to query parameters (12 projects per page)', + parameters=[ + OpenApiParameter( + 'id', + location=OpenApiParameter.QUERY, + description='A unique number value identifying this project', + type=OpenApiTypes.NUMBER + ), + OpenApiParameter( + 'name', + location=OpenApiParameter.QUERY, + description='Find all projects where name contains a parameter value', + type=OpenApiTypes.STR + ), + OpenApiParameter( + 'owner', + location=OpenApiParameter.QUERY, + description='Find all project where owner name contains a parameter value', + type=OpenApiTypes.STR + ), + OpenApiParameter( + 'status', + location=OpenApiParameter.QUERY, + description='Find all projects with a specific status', + type=OpenApiTypes.STR, + enum=StatusChoice.list() + ), + OpenApiParameter( + 'names_only', + location=OpenApiParameter.QUERY, + description="Returns only names and id's of projects", + type=OpenApiTypes.BOOL + ) + ], + #TODO + responses={ + '200': OpenApiResponse(description=''), + }, + tags=['projects'], + versions=['v1'], +)) +@method_decorator(name='create', decorator=extend_schema( + summary='Method creates a new project', + #TODO + responses={ + '200': OpenApiResponse(description=''), + }, + tags=['projects'], + versions=['v1'], +)) +@method_decorator(name='retrieve', decorator=extend_schema( + summary='Method returns details of a specific project', + #TODO + responses={ + '200': OpenApiResponse(description=''), + }, + tags=['projects'], + versions=['v1'], +)) +@method_decorator(name='destroy', decorator=extend_schema( + summary='Method deletes a specific project', + #TODO + responses={ + '204': OpenApiResponse(description=''), + }, + tags=['projects'], + versions=['v1'], +)) +@method_decorator(name='partial_update', decorator=extend_schema( + summary='Methods does a partial update of chosen fields in a project', + #TODO + responses={ + '200': OpenApiResponse(description=''), + }, + tags=['projects'], + versions=['v1'], +)) class ProjectViewSet(viewsets.ModelViewSet): queryset = models.Project.objects.prefetch_related(Prefetch('label_set', queryset=models.Label.objects.order_by('id') @@ -265,10 +371,15 @@ def perform_create(self, serializer): serializer.save(owner=self.request.user, organization=self.request.iam_context['organization']) - @swagger_auto_schema( - method='get', - operation_summary='Returns information of the tasks of the project with the selected id', - responses={'200': TaskSerializer(many=True)}) + @extend_schema( + methods=['GET'], + summary='Returns information of the tasks of the project with the selected id', + responses={ + '200': TaskSerializer(many=True) + }, + tags=['projects'], + versions=['v1'], + ) @action(detail=True, methods=['GET'], serializer_class=TaskSerializer) def tasks(self, request, pk): self.get_object() # force to call check_object_permissions @@ -285,34 +396,61 @@ def tasks(self, request, pk): return Response(serializer.data) - @swagger_auto_schema(method='get', operation_summary='Export project as a dataset in a specific format', - manual_parameters=[ - openapi.Parameter('format', openapi.IN_QUERY, - description="Desired output format name\nYou can get the list of supported formats at:\n/server/annotation/formats", - type=openapi.TYPE_STRING, required=True), - openapi.Parameter('filename', openapi.IN_QUERY, - description="Desired output file name", - type=openapi.TYPE_STRING, required=False), - openapi.Parameter('action', in_=openapi.IN_QUERY, + @extend_schema( + methods=['GET'], + summary='Export project as a dataset in a specific format', + parameters=[ + OpenApiParameter( + 'format', + location=OpenApiParameter.QUERY, + description='Desired output format name\nYou can get the list of supported formats at:\n/server/annotation/formats', + type=OpenApiTypes.STR, + required=True + ), + OpenApiParameter( + 'filename', + location=OpenApiParameter.QUERY, + description='Desired output file name', + type=OpenApiTypes.STR, + required=False + ), + OpenApiParameter( + 'action', + location=OpenApiParameter.QUERY, description='Used to start downloading process after annotation file had been created', - type=openapi.TYPE_STRING, required=False, enum=['download', 'import_status']) + type=OpenApiTypes.STR, + required=False, + enum=['download', 'import_status'] + ) ], - responses={'202': openapi.Response(description='Exporting has been started'), - '201': openapi.Response(description='Output file is ready for downloading'), - '200': openapi.Response(description='Download of file started'), - '405': openapi.Response(description='Format is not available'), - } + responses={ + '202': OpenApiResponse(description='Exporting has been started'), + '201': OpenApiResponse(description='Output file is ready for downloading'), + '200': OpenApiResponse(description='Download of file started'), + '405': OpenApiResponse(description='Format is not available'), + }, + tags=['projects'], + versions=['v1'], ) - @swagger_auto_schema(method='post', operation_summary='Import dataset in specific format as a project', - manual_parameters=[ - openapi.Parameter('format', openapi.IN_QUERY, - description="Desired dataset format name\nYou can get the list of supported formats at:\n/server/annotation/formats", - type=openapi.TYPE_STRING, required=True) + @extend_schema( + methods=['POST'], + summary='Import dataset in specific format as a project', + parameters=[ + OpenApiParameter( + 'format', + location=OpenApiParameter.QUERY, + description='Desired dataset format name\nYou can get the list of supported formats at:\n/server/annotation/formats', + type=OpenApiTypes.STR, + required=True + ) ], - responses={'202': openapi.Response(description='Exporting has been started'), - '400': openapi.Response(description='Failed to import dataset'), - '405': openapi.Response(description='Format is not available'), - } + responses={ + '202': OpenApiResponse(description='Exporting has been started'), + '400': OpenApiResponse(description='Failed to import dataset'), + '405': OpenApiResponse(description='Format is not available'), + }, + tags=['projects'], + versions=['v1'], ) @action(detail=True, methods=['GET', 'POST'], serializer_class=None, url_path='dataset') @@ -366,25 +504,42 @@ def dataset(self, request, pk): filename=request.query_params.get("filename", "").lower(), ) - @swagger_auto_schema(method='get', operation_summary='Method allows to download project annotations', - manual_parameters=[ - openapi.Parameter('format', openapi.IN_QUERY, - description="Desired output format name\nYou can get the list of supported formats at:\n/server/annotation/formats", - type=openapi.TYPE_STRING, required=True), - openapi.Parameter('filename', openapi.IN_QUERY, - description="Desired output file name", - type=openapi.TYPE_STRING, required=False), - openapi.Parameter('action', in_=openapi.IN_QUERY, + @extend_schema( + methods=['GET'], + summary='Method allows to download project annotations', + parameters=[ + OpenApiParameter( + 'format', + location=OpenApiParameter.QUERY, + description='Desired output format name\nYou can get the list of supported formats at:\n/server/annotation/formats', + type=OpenApiTypes.STR, + required=True + ), + OpenApiParameter( + 'filename', + location=OpenApiParameter.QUERY, + description='Desired output file name', + type=OpenApiTypes.STR, + required=False + ), + OpenApiParameter( + 'action', + location=OpenApiParameter.QUERY, description='Used to start downloading process after annotation file had been created', - type=openapi.TYPE_STRING, required=False, enum=['download']) + type=OpenApiTypes.STR, + required=False, + enum=['download'] + ) ], responses={ - '202': openapi.Response(description='Dump of annotations has been started'), - '201': openapi.Response(description='Annotations file is ready to download'), - '200': openapi.Response(description='Download of file started'), - '405': openapi.Response(description='Format is not available'), - '401': openapi.Response(description='Format is not specified'), - } + '202': OpenApiResponse(description='Dump of annotations has been started'), + '201': OpenApiResponse(description='Annotations file is ready to download'), + '200': OpenApiResponse(description='Download of file started'), + '405': OpenApiResponse(description='Format is not available'), + '401': OpenApiResponse(description='Format is not specified'), + }, + tags=['projects'], + versions=['v1'], ) @action(detail=True, methods=['GET'], serializer_class=LabeledDataSerializer) @@ -518,35 +673,105 @@ class Meta: fields = ("id", "project_id", "project", "name", "owner", "mode", "status", "assignee") -class DjangoFilterInspector(CoreAPICompatInspector): - def get_filter_parameters(self, filter_backend): - if isinstance(filter_backend, DjangoFilterBackend): - result = super(DjangoFilterInspector, self).get_filter_parameters(filter_backend) - res = result.copy() - - for param in result: - if param.get('name') == 'project_id' or param.get('name') == 'project': - res.remove(param) - return res - - return NotHandled - -@method_decorator(name='list', decorator=swagger_auto_schema( - operation_summary='Returns a paginated list of tasks according to query parameters (10 tasks per page)', - manual_parameters=[ - openapi.Parameter('id',openapi.IN_QUERY,description="A unique number value identifying this task",type=openapi.TYPE_NUMBER), - openapi.Parameter('name', openapi.IN_QUERY, description="Find all tasks where name contains a parameter value", type=openapi.TYPE_STRING), - openapi.Parameter('owner', openapi.IN_QUERY, description="Find all tasks where owner name contains a parameter value", type=openapi.TYPE_STRING), - openapi.Parameter('mode', openapi.IN_QUERY, description="Find all tasks with a specific mode", type=openapi.TYPE_STRING, enum=['annotation', 'interpolation']), - openapi.Parameter('status', openapi.IN_QUERY, description="Find all tasks with a specific status", type=openapi.TYPE_STRING,enum=['annotation','validation','completed']), - openapi.Parameter('assignee', openapi.IN_QUERY, description="Find all tasks where assignee name contains a parameter value", type=openapi.TYPE_STRING) +# class DjangoFilterInspector(CoreAPICompatInspector): +# def get_filter_parameters(self, filter_backend): +# if isinstance(filter_backend, DjangoFilterBackend): +# result = super(DjangoFilterInspector, self).get_filter_parameters(filter_backend) +# res = result.copy() + +# for param in result: +# if param.get('name') == 'project_id' or param.get('name') == 'project': +# res.remove(param) +# return res + +# return NotHandled + +@method_decorator(name='list', decorator=extend_schema( + summary='Returns a paginated list of tasks according to query parameters (10 tasks per page)', + parameters=[ + OpenApiParameter( + 'id', + location=OpenApiParameter.QUERY, + description='A unique number value identifying this task', + type=OpenApiTypes.NUMBER + ), + OpenApiParameter( + 'name', + location=OpenApiParameter.QUERY, + description='Find all tasks where name contains a parameter value', + type=OpenApiTypes.STR + ), + OpenApiParameter( + 'owner', + location=OpenApiParameter.QUERY, + description='Find all tasks where owner name contains a parameter value', + type=OpenApiTypes.STR + ), + OpenApiParameter( + 'mode', + location=OpenApiParameter.QUERY, + description='Find all tasks with a specific mode', + type=OpenApiTypes.STR, + enum=['annotation', 'interpolation'] + ), + OpenApiParameter( + 'status', + location=OpenApiParameter.QUERY, + description='Find all tasks with a specific status', + type=OpenApiTypes.STR, + enum=StatusChoice.list() + ), + OpenApiParameter( + 'assignee', + location=OpenApiParameter.QUERY, + description='Find all tasks where assignee name contains a parameter value', + type=OpenApiTypes.STR + ) ], - filter_inspectors=[DjangoFilterInspector])) -@method_decorator(name='create', decorator=swagger_auto_schema(operation_summary='Method creates a new task in a database without any attached images and videos')) -@method_decorator(name='retrieve', decorator=swagger_auto_schema(operation_summary='Method returns details of a specific task')) -@method_decorator(name='update', decorator=swagger_auto_schema(operation_summary='Method updates a task by id')) -@method_decorator(name='destroy', decorator=swagger_auto_schema(operation_summary='Method deletes a specific task, all attached jobs, annotations, and data')) -@method_decorator(name='partial_update', decorator=swagger_auto_schema(operation_summary='Methods does a partial update of chosen fields in a task')) + #filter_inspectors=[DjangoFilterInspector] + tags=['tasks'], + versions=['v1'], +)) +@method_decorator(name='create', decorator=extend_schema( + summary='Method creates a new task in a database without any attached images and videos', + responses={ + '202': OpenApiResponse(description=''), + '201': OpenApiResponse(description=''), + '200': OpenApiResponse(description=''), + }, + tags=['tasks'], + versions=['v1'], +)) +@method_decorator(name='retrieve', decorator=extend_schema( + summary='Method returns details of a specific task', + responses=OpenApiResponse(description=''), + tags=['tasks'], + versions=['v1'], +)) +@method_decorator(name='update', decorator=extend_schema( + summary='Method updates a task by id', + responses={ + '200':OpenApiResponse(description=''), # or serializer + }, + tags=['tasks'], + versions=['v1'], +)) +@method_decorator(name='destroy', decorator=extend_schema( + summary='Method deletes a specific task, all attached jobs, annotations, and data', + responses={ + '204': OpenApiResponse('The task has been deleted'), + }, + tags=['tasks'], + versions=['v1'], +)) +@method_decorator(name='partial_update', decorator=extend_schema( + summary='Methods does a partial update of chosen fields in a task', + responses={ + '200': OpenApiResponse(description=''), # or serializer + }, + tags=['tasks'], + versions=['v1'], +)) class TaskViewSet(UploadMixin, viewsets.ModelViewSet): queryset = Task.objects.prefetch_related( Prefetch('label_set', queryset=models.Label.objects.order_by('id')), @@ -566,10 +791,36 @@ def get_queryset(self): return queryset + @extend_schema( + methods=['POST'], + summary='Method recreates a task from an attached task backup file', + #TODO + responses={ + '202': OpenApiResponse(description=''), + '201': OpenApiResponse(description=''), + '400': OpenApiResponse(description=''), + '500': OpenApiResponse(description=''), + }, + tags=['tasks'], + versions=['v1'], + ) @action(detail=False, methods=['POST'], url_path='backup') def import_backup(self, request, pk=None): return backup.import_task(request) + @extend_schema( + methods=['GET'], + summary='Method backup a specified task', + #TODO + responses={ + '202': OpenApiResponse(description=''), + '201': OpenApiResponse(description=''), + '400': OpenApiResponse(description=''), + '500': OpenApiResponse(description=''), + }, + tags=['tasks'], + versions=['v1'], + ) @action(methods=['GET'], detail=True, url_path='backup') def export_backup(self, request, pk=None): db_task = self.get_object() # force to call check_object_permissions @@ -601,10 +852,15 @@ def perform_destroy(self, instance): db_project = instance.project db_project.save() - @swagger_auto_schema( - method='get', - operation_summary='Returns a list of jobs for a specific task', - responses={'200': JobReadSerializer(many=True)}) + @extend_schema( + methods=['GET'], + summary='Returns a list of jobs for a specific task', + responses={ + '200': JobReadSerializer(many=True), + }, + tags=['tasks'], + versions=['v1'], + ) @action(detail=True, methods=['GET'], serializer_class=JobReadSerializer) def jobs(self, request, pk): self.get_object() # force to call check_object_permissions @@ -648,28 +904,38 @@ def upload_finished(self, request): task.create(db_task.id, data) return Response(serializer.data, status=status.HTTP_202_ACCEPTED) - @swagger_auto_schema(method='post', operation_summary='Method permanently attaches images or video to a task. Supports tus uploads, see more https://tus.io/', - request_body=DataSerializer, - manual_parameters=[ - openapi.Parameter('Upload-Start', in_=openapi.IN_HEADER, type=openapi.TYPE_BOOLEAN, - description="Initializes data upload. No data should be sent with this header"), - openapi.Parameter('Upload-Multiple', in_=openapi.IN_HEADER, type=openapi.TYPE_BOOLEAN, - description="Indicates that data with this request are single or multiple files that should be attached to a task"), - openapi.Parameter('Upload-Finish', in_=openapi.IN_HEADER, type=openapi.TYPE_BOOLEAN, - description="Finishes data upload. Can be combined with Upload-Start header to create task data with one request"), - ] + @extend_schema( + methods='post', + summary='Method permanently attaches images or video to a task. Supports tus uploads, see more https://tus.io/', + request=DataSerializer, + parameters=[ + OpenApiParameter('Upload-Start', location=OpenApiParameter.HEADER, type=OpenApiTypes.BOOL, + description='Initializes data upload. No data should be sent with this header'), + OpenApiParameter('Upload-Multiple', location=OpenApiParameter.HEADER, type=OpenApiTypes.BOOL, + description='Indicates that data with this request are single or multiple files that should be attached to a task'), + OpenApiParameter('Upload-Finish', location=OpenApiParameter.HEADER, type=OpenApiTypes.BOOL, + description='Finishes data upload. Can be combined with Upload-Start header to create task data with one request'), + ], + #TODO + responses={}, + tags=['tasks'], + versions=['v1'], ) - @swagger_auto_schema(method='get', operation_summary='Method returns data for a specific task', - manual_parameters=[ - openapi.Parameter('type', in_=openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING, + @extend_schema(methods=['OPTIONS', 'POST', 'GET'], summary='Method returns data for a specific task', + parameters=[ + OpenApiParameter('type', location=OpenApiParameter.QUERY, required=True, type=OpenApiTypes.STR, enum=['chunk', 'frame', 'preview', 'context_image'], - description="Specifies the type of the requested data"), - openapi.Parameter('quality', in_=openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING, + description='Specifies the type of the requested data'), + OpenApiParameter('quality', location=OpenApiParameter.QUERY, required=True, type=OpenApiTypes.STR, enum=['compressed', 'original'], description="Specifies the quality level of the requested data, doesn't matter for 'preview' type"), - openapi.Parameter('number', in_=openapi.IN_QUERY, required=True, type=openapi.TYPE_NUMBER, + OpenApiParameter('number', location=OpenApiParameter.QUERY, required=True, type=OpenApiTypes.NUMBER, description="A unique number value identifying chunk or frame, doesn't matter for 'preview' type"), - ] + ], + #TODO + responses={}, + tags=['tasks'], + versions=['v1'], ) @action(detail=True, methods=['OPTIONS', 'POST', 'GET'], url_path=r'data/?$') def data(self, request, pk): @@ -697,41 +963,54 @@ def data(self, request, pk): return data_getter(request, db_task.data.start_frame, db_task.data.stop_frame, db_task.data) - @swagger_auto_schema(method='get', operation_summary='Method allows to download task annotations', - manual_parameters=[ - openapi.Parameter('format', openapi.IN_QUERY, + @extend_schema(methods=['GET'], summary='Method allows to download task annotations', + parameters=[ + OpenApiParameter('format', location=OpenApiParameter.QUERY, description="Desired output format name\nYou can get the list of supported formats at:\n/server/annotation/formats", - type=openapi.TYPE_STRING, required=False), - openapi.Parameter('filename', openapi.IN_QUERY, + type=OpenApiTypes.STR, required=False), + OpenApiParameter('filename', location=OpenApiParameter.QUERY, description="Desired output file name", - type=openapi.TYPE_STRING, required=False), - openapi.Parameter('action', in_=openapi.IN_QUERY, + type=OpenApiTypes.STR, required=False), + OpenApiParameter('action', location=OpenApiParameter.QUERY, description='Used to start downloading process after annotation file had been created', - type=openapi.TYPE_STRING, required=False, enum=['download']) + type=OpenApiTypes.STR, required=False, enum=['download']) ], responses={ - '202': openapi.Response(description='Dump of annotations has been started'), - '201': openapi.Response(description='Annotations file is ready to download'), - '200': openapi.Response(description='Download of file started'), - '405': openapi.Response(description='Format is not available'), - } + '202': OpenApiResponse(description='Dump of annotations has been started'), + '201': OpenApiResponse(description='Annotations file is ready to download'), + '200': OpenApiResponse(description='Download of file started'), + '405': OpenApiResponse(description='Format is not available'), + }, + tags=['tasks'], + versions=['v1'], ) - @swagger_auto_schema(method='put', operation_summary='Method allows to upload task annotations', - manual_parameters=[ - openapi.Parameter('format', openapi.IN_QUERY, - description="Input format name\nYou can get the list of supported formats at:\n/server/annotation/formats", - type=openapi.TYPE_STRING, required=False), + @extend_schema(methods=['PUT'], summary='Method allows to upload task annotations', + parameters=[ + OpenApiParameter('format', location=OpenApiParameter.QUERY, + description='Input format name\nYou can get the list of supported formats at:\n/server/annotation/formats', + type=OpenApiTypes.STR, required=False), ], responses={ - '202': openapi.Response(description='Uploading has been started'), - '201': openapi.Response(description='Uploading has finished'), - '405': openapi.Response(description='Format is not available'), - } + '202': OpenApiResponse(description='Uploading has been started'), + '201': OpenApiResponse(description='Uploading has finished'), + '405': OpenApiResponse(description='Format is not available'), + }, + tags=['tasks'], + versions=['v1'], + ) + @extend_schema(methods=['PATCH'], summary='Method performs a partial update of annotations in a specific task', + parameters=[OpenApiParameter('action', location=OpenApiParameter.QUERY, required=True, type=OpenApiTypes.STR, + enum=['create', 'update', 'delete'])], + tags=['tasks'], + versions=['v1'], + ) + @extend_schema(methods=['DELETE'], summary='Method deletes all annotations for a specific task', + responses={ + '204': OpenApiResponse(description='The annotation has been deleted'), + }, + tags=['tasks'], + versions=['v1'], ) - @swagger_auto_schema(method='patch', operation_summary='Method performs a partial update of annotations in a specific task', - manual_parameters=[openapi.Parameter('action', in_=openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING, - enum=['create', 'update', 'delete'])]) - @swagger_auto_schema(method='delete', operation_summary='Method deletes all annotations for a specific task') @action(detail=True, methods=['GET', 'DELETE', 'PUT', 'PATCH'], serializer_class=LabeledDataSerializer) def annotations(self, request, pk): @@ -783,7 +1062,16 @@ def annotations(self, request, pk): return Response(data=str(e), status=status.HTTP_400_BAD_REQUEST) return Response(data) - @swagger_auto_schema(method='get', operation_summary='When task is being created the method returns information about a status of the creation process') + @extend_schema( + methods=['GET'], + summary='When task is being created the method returns information about a status of the creation process', + #TODO + responses={ + '200': OpenApiResponse(description=''), + }, + tags=['tasks'], + versions=['v1'], + ) @action(detail=True, methods=['GET'], serializer_class=RqStatusSerializer) def status(self, request, pk): self.get_object() # force to call check_object_permissions @@ -813,8 +1101,8 @@ def _get_rq_response(queue, job_id): return response @staticmethod - @swagger_auto_schema(method='get', operation_summary='Method provides a meta information about media files which are related with the task', - responses={'200': DataMetaSerializer()}) + # @extend_schema(methods=['GET'], summary='Method provides a meta information about media files which are related with the task', + # responses={'200': DataMetaSerializer()}) @action(detail=True, methods=['GET'], serializer_class=DataMetaSerializer, url_path='data/meta') def data_info(request, pk): @@ -842,24 +1130,24 @@ def data_info(request, pk): serializer = DataMetaSerializer(db_data) return Response(serializer.data) - @swagger_auto_schema(method='get', operation_summary='Export task as a dataset in a specific format', - manual_parameters=[ - openapi.Parameter('format', openapi.IN_QUERY, - description="Desired output format name\nYou can get the list of supported formats at:\n/server/annotation/formats", - type=openapi.TYPE_STRING, required=True), - openapi.Parameter('filename', openapi.IN_QUERY, - description="Desired output file name", - type=openapi.TYPE_STRING, required=False), - openapi.Parameter('action', in_=openapi.IN_QUERY, - description='Used to start downloading process after annotation file had been created', - type=openapi.TYPE_STRING, required=False, enum=['download']) - ], - responses={'202': openapi.Response(description='Exporting has been started'), - '201': openapi.Response(description='Output file is ready for downloading'), - '200': openapi.Response(description='Download of file started'), - '405': openapi.Response(description='Format is not available'), - } - ) + # @extend_schema(methods=['GET'], summary='Export task as a dataset in a specific format', + # parameters=[ + # OpenApiParameter('format', OpenApiParameter.QUERY, + # description="Desired output format name\nYou can get the list of supported formats at:\n/server/annotation/formats", + # type=OpenApiTypes.STR, required=True), + # OpenApiParameter('filename', OpenApiParameter.QUERY, + # description="Desired output file name", + # type=OpenApiTypes.STR, required=False), + # OpenApiParameter('action', location=OpenApiParameter.QUERY, + # description='Used to start downloading process after annotation file had been created', + # type=OpenApiTypes.STR, required=False, enum=['download']) + # ], + # responses={'202': OpenApiResponse(description='Exporting has been started'), + # '201': OpenApiResponse(description='Output file is ready for downloading'), + # '200': OpenApiResponse(description='Download of file started'), + # '405': OpenApiResponse(description='Format is not available'), + # } + # ) @action(detail=True, methods=['GET'], serializer_class=None, url_path='dataset') def dataset_export(self, request, pk): @@ -875,10 +1163,10 @@ def dataset_export(self, request, pk): filename=request.query_params.get("filename", "").lower(), ) -@method_decorator(name='retrieve', decorator=swagger_auto_schema(operation_summary='Method returns details of a job')) -@method_decorator(name='update', decorator=swagger_auto_schema(operation_summary='Method updates a job by id')) -@method_decorator(name='partial_update', decorator=swagger_auto_schema( - operation_summary='Methods does a partial update of chosen fields in a job')) +# @method_decorator(name='retrieve', decorator=extend_schema(summary='Method returns details of a job')) +# @method_decorator(name='update', decorator=extend_schema(summary='Method updates a job by id')) +# @method_decorator(name='partial_update', decorator=extend_schema( +# summary='Methods does a partial update of chosen fields in a job')) class JobViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin): queryset = Job.objects.all().order_by('id') @@ -897,15 +1185,15 @@ def get_serializer_class(self): else: return JobWriteSerializer - @swagger_auto_schema(method='get', operation_summary='Method returns annotations for a specific job') - @swagger_auto_schema(method='put', operation_summary='Method performs an update of all annotations in a specific job') - @swagger_auto_schema(method='patch', manual_parameters=[ - openapi.Parameter('action', in_=openapi.IN_QUERY, type=openapi.TYPE_STRING, required=True, - enum=['create', 'update', 'delete'])], - operation_summary='Method performs a partial update of annotations in a specific job') - @swagger_auto_schema(method='delete', operation_summary='Method deletes all annotations for a specific job') - @action(detail=True, methods=['GET', 'DELETE', 'PUT', 'PATCH'], - serializer_class=LabeledDataSerializer) + # @extend_schema(methods=['GET'], summary='Method returns annotations for a specific job') + # @extend_schema(method='put', summary='Method performs an update of all annotations in a specific job') + # @extend_schema(methods=['PATCH'], parameters=[ + # OpenApiParameter('action', location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=True, + # enum=['create', 'update', 'delete'])], + # summary='Method performs a partial update of annotations in a specific job') + # @extend_schema(methods=['DELETE'], summary='Method deletes all annotations for a specific job') + # @action(detail=True, methods=['GET', 'DELETE', 'PUT', 'PATCH'], + # serializer_class=LabeledDataSerializer) def annotations(self, request, pk): self.get_object() # force to call check_object_permissions if request.method == 'GET': @@ -945,10 +1233,10 @@ def annotations(self, request, pk): return Response(data=str(e), status=status.HTTP_400_BAD_REQUEST) return Response(data) - @swagger_auto_schema( - method='get', - operation_summary='Method returns list of issues for the job', - responses={'200': IssueReadSerializer(many=True)}) + # @extend_schema( + # method='get', + # summary='Method returns list of issues for the job', + # responses={'200': IssueReadSerializer(many=True)}) @action(detail=True, methods=['GET'], serializer_class=IssueReadSerializer) def issues(self, request, pk): db_job = self.get_object() @@ -958,18 +1246,18 @@ def issues(self, request, pk): return Response(serializer.data) - @swagger_auto_schema(method='get', operation_summary='Method returns data for a specific job', - manual_parameters=[ - openapi.Parameter('type', in_=openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING, - enum=['chunk', 'frame', 'preview', 'context_image'], - description="Specifies the type of the requested data"), - openapi.Parameter('quality', in_=openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING, - enum=['compressed', 'original'], - description="Specifies the quality level of the requested data, doesn't matter for 'preview' type"), - openapi.Parameter('number', in_=openapi.IN_QUERY, required=True, type=openapi.TYPE_NUMBER, - description="A unique number value identifying chunk or frame, doesn't matter for 'preview' type"), - ] - ) + # @extend_schema(methods=['GET'], summary='Method returns data for a specific job', + # parameters=[ + # OpenApiParameter('type', location=OpenApiParameter.QUERY, required=True, type=OpenApiTypes.STR, + # enum=['chunk', 'frame', 'preview', 'context_image'], + # description="Specifies the type of the requested data"), + # OpenApiParameter('quality', location=OpenApiParameter.QUERY, required=True, type=OpenApiTypes.STR, + # enum=['compressed', 'original'], + # description="Specifies the quality level of the requested data, doesn't matter for 'preview' type"), + # OpenApiParameter('number', location=OpenApiParameter.QUERY, required=True, type=OpenApiTypes.NUMBER, + # description="A unique number value identifying chunk or frame, doesn't matter for 'preview' type"), + # ] + # ) @action(detail=True) def data(self, request, pk): db_job = self.get_object() # call check_object_permissions as well @@ -1004,10 +1292,10 @@ def get_serializer_class(self): def perform_create(self, serializer): serializer.save(owner=self.request.user) - @swagger_auto_schema( - method='get', - operation_summary='The action returns all comments of a specific issue', - responses={'200': CommentReadSerializer(many=True)}) + # @extend_schema( + # method='get', + # summary='The action returns all comments of a specific issue', + # responses={'200': CommentReadSerializer(many=True)}) @action(detail=True, methods=['GET'], serializer_class=CommentReadSerializer) def comments(self, request, pk): db_issue = self.get_object() @@ -1043,18 +1331,18 @@ class Meta: model = User fields = ("id", "is_active") -@method_decorator(name='list', decorator=swagger_auto_schema( - manual_parameters=[ - openapi.Parameter('id',openapi.IN_QUERY,description="A unique number value identifying this user",type=openapi.TYPE_NUMBER), - openapi.Parameter('is_active',openapi.IN_QUERY,description="Returns only active users",type=openapi.TYPE_BOOLEAN), - ], - operation_summary='Method provides a paginated list of users registered on the server')) -@method_decorator(name='retrieve', decorator=swagger_auto_schema( - operation_summary='Method provides information of a specific user')) -@method_decorator(name='partial_update', decorator=swagger_auto_schema( - operation_summary='Method updates chosen fields of a user')) -@method_decorator(name='destroy', decorator=swagger_auto_schema( - operation_summary='Method deletes a specific user from the server')) +# @method_decorator(name='list', decorator=extend_schema( +# parameters=[ +# OpenApiParameter('id',OpenApiParameter.QUERY,description="A unique number value identifying this user",type=OpenApiTypes.NUMBER), +# OpenApiParameter('is_active',OpenApiParameter.QUERY,description="Returns only active users",type=OpenApiTypes.BOOL), +# ], +# summary='Method provides a paginated list of users registered on the server')) +# @method_decorator(name='retrieve', decorator=extend_schema( +# summary='Method provides information of a specific user')) +# @method_decorator(name='partial_update', decorator=extend_schema( +# summary='Method updates chosen fields of a user')) +# @method_decorator(name='destroy', decorator=extend_schema( +# summary='Method deletes a specific user from the server')) class UserViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin): queryset = User.objects.prefetch_related('groups').all().order_by('id') @@ -1082,7 +1370,7 @@ def get_serializer_class(self): else: return BasicUserSerializer - @swagger_auto_schema(method='get', operation_summary='Method returns an instance of a user who is currently authorized') + # @extend_schema(methods=['GET'], summary='Method returns an instance of a user who is currently authorized') @action(detail=False, methods=['GET']) def self(self, request): """ @@ -1092,14 +1380,14 @@ def self(self, request): serializer = serializer_class(request.user, context={ "request": request }) return Response(serializer.data) -class RedefineDescriptionField(FieldInspector): - # pylint: disable=no-self-use - def process_result(self, result, method_name, obj, **kwargs): - if isinstance(result, openapi.Schema): - if hasattr(result, 'title') and result.title == 'Specific attributes': - result.description = 'structure like key1=value1&key2=value2\n' \ - 'supported: range=aws_range' - return result +# class RedefineDescriptionField(FieldInspector): +# # pylint: disable=no-self-use +# def process_result(self, result, method_name, obj, **kwargs): +# if isinstance(result, openapi.Schema): +# if hasattr(result, 'title') and result.title == 'Specific attributes': +# result.description = 'structure like key1=value1&key2=value2\n' \ +# 'supported: range=aws_range' +# return result class CloudStorageFilter(filters.FilterSet): display_name = filters.CharFilter(field_name='display_name', lookup_expr='icontains') @@ -1115,38 +1403,80 @@ class Meta: @method_decorator( name='retrieve', - decorator=swagger_auto_schema( - operation_summary='Method returns details of a specific cloud storage', + decorator=extend_schema( + operation_id="CloudStorage - Retrieve", + summary='Method returns details of a specific cloud storage', + #request='application/json', responses={ - '200': openapi.Response(description='A details of a storage'), + '200': OpenApiResponse(description='A details of a storage'), }, - tags=['cloud storages'] + tags=['cloud storages'], + versions=['v1'], ) ) -@method_decorator(name='list', decorator=swagger_auto_schema( - operation_summary='Returns a paginated list of storages according to query parameters', - manual_parameters=[ - openapi.Parameter('provider_type', openapi.IN_QUERY, description="A supported provider of cloud storages", - type=openapi.TYPE_STRING, enum=CloudProviderChoice.list()), - openapi.Parameter('display_name', openapi.IN_QUERY, description="A display name of storage", type=openapi.TYPE_STRING), - openapi.Parameter('resource', openapi.IN_QUERY, description="A name of bucket or container", type=openapi.TYPE_STRING), - openapi.Parameter('owner', openapi.IN_QUERY, description="A resource owner", type=openapi.TYPE_STRING), - openapi.Parameter('credentials_type', openapi.IN_QUERY, description="A type of a granting access", type=openapi.TYPE_STRING, enum=CredentialsTypeChoice.list()), +@method_decorator(name='list', decorator=extend_schema( + operation_id='CloudStorage - List', + summary='Returns a paginated list of storages according to query parameters', + parameters=[ + OpenApiParameter( + 'provider_type', + location=OpenApiParameter.QUERY, + description='A supported provider of cloud storages', + type=OpenApiTypes.STR, + enum=CloudProviderChoice.list() + ), + OpenApiParameter( + 'display_name', + location=OpenApiParameter.QUERY, + description='A display name of storage', + type=OpenApiTypes.STR + ), + OpenApiParameter( + 'resource', + location=OpenApiParameter.QUERY, + description='A name of bucket or container', + type=OpenApiTypes.STR + ), + OpenApiParameter( + 'owner', + location=OpenApiParameter.QUERY, + description='A resource owner', + type=OpenApiTypes.STR + ), + OpenApiParameter( + 'credentials_type', + location=OpenApiParameter.QUERY, + description='A type of a granting access', + type=OpenApiTypes.STR, + enum=CredentialsTypeChoice.list() + ), ], - responses={'200': BaseCloudStorageSerializer(many=True)}, + responses={ + '200': BaseCloudStorageSerializer(many=True) + }, tags=['cloud storages'], - field_inspectors=[RedefineDescriptionField] + versions=['v1'], + #FIXME + #field_inspectors=[RedefineDescriptionField] ) ) -@method_decorator(name='destroy', decorator=swagger_auto_schema( - operation_summary='Method deletes a specific cloud storage', - tags=['cloud storages'] +@method_decorator(name='destroy', decorator=extend_schema( + operation_id='CloudStorage - Delete', + summary='Method deletes a specific cloud storage', + responses={ + '204': OpenApiResponse(description='The cloud storage has been removed'), + }, + tags=['cloud storages'], + versions=['v1'], ) ) -@method_decorator(name='partial_update', decorator=swagger_auto_schema( - operation_summary='Methods does a partial update of chosen fields in a cloud storage instance', +@method_decorator(name='partial_update', decorator=extend_schema( + operation_id='CloudStorage - PartialUpdate', + summary='Methods does a partial update of chosen fields in a cloud storage instance', + responses={}, tags=['cloud storages'], - field_inspectors=[RedefineDescriptionField] + versions=['v1'], + #field_inspectors=[RedefineDescriptionField] ) ) class CloudStorageViewSet(viewsets.ModelViewSet): @@ -1156,7 +1486,7 @@ class CloudStorageViewSet(viewsets.ModelViewSet): filterset_class = CloudStorageFilter def get_serializer_class(self): - if self.request.method in ("POST", "PATCH"): + if self.request.method in ('POST', 'PATCH'): return CloudStorageSerializer else: return BaseCloudStorageSerializer @@ -1184,13 +1514,15 @@ def perform_destroy(self, instance): super().perform_destroy(instance) shutil.rmtree(cloud_storage_dirname, ignore_errors=True) - @method_decorator(name='create', decorator=swagger_auto_schema( - operation_summary='Method creates a cloud storage with a specified characteristics', + @method_decorator(name='create', decorator=extend_schema( + operation_id='CloudStorage - Create', + summary='Method creates a cloud storage with a specified characteristics', responses={ - '201': openapi.Response(description='A storage has beed created') + '201': OpenApiResponse(description='A storage has beed created') }, tags=['cloud storages'], - field_inspectors=[RedefineDescriptionField], + versions=['v1'], + #field_inspectors=[RedefineDescriptionField], ) ) def create(self, request, *args, **kwargs): @@ -1211,18 +1543,26 @@ def create(self, request, *args, **kwargs): response = HttpResponseBadRequest(str(ex)) return response - @swagger_auto_schema( - method='get', - operation_summary='Method returns a manifest content', - manual_parameters=[ - openapi.Parameter('manifest_path', openapi.IN_QUERY, - description="Path to the manifest file in a cloud storage", - type=openapi.TYPE_STRING) + @extend_schema( + methods=['GET'], + summary='Method returns a manifest content', + parameters=[ + OpenApiParameter( + 'manifest_path', + location=OpenApiParameter.QUERY, + description='Path to the manifest file in a cloud storage', + type=OpenApiTypes.STR + ) ], responses={ - '200': openapi.Response(description='A manifest content'), + '200': OpenApiResponse(description='A manifest content'), + # TODO: describee all avaliable cases + '404': OpenApiResponse(description='The specified storage does not exist' + 'or real cloud storage was removed or ' + 'Specified manifest file does not exist on the cloud storage') }, - tags=['cloud storages'] + tags=['cloud storages'], + versions=['v1'], ) @action(detail=True, methods=['GET'], url_path='content') def content(self, request, pk): @@ -1280,13 +1620,16 @@ def content(self, request, pk): msg = str(ex) return HttpResponseBadRequest(msg) - @swagger_auto_schema( - method='get', - operation_summary='Method returns a preview image from a cloud storage', + @extend_schema( + methods=['GET'], + summary='Method returns a preview image from a cloud storage', responses={ - '200': openapi.Response(description='Preview'), + '200': OpenApiResponse(description='Cloud Storage preview'), + #TODO: + '404': OpenApiResponse(description=''), }, - tags=['cloud storages'] + tags=['cloud storages'], + versions=['v1'], ) @action(detail=True, methods=['GET'], url_path='preview') def preview(self, request, pk): @@ -1360,13 +1703,16 @@ def preview(self, request, pk): msg = str(ex) return HttpResponseBadRequest(msg) - @swagger_auto_schema( - method='get', - operation_summary='Method returns a cloud storage status', + @extend_schema( + methods=['GET'], + summary='Method returns a cloud storage status', responses={ - '200': openapi.Response(description='Status'), + '200': OpenApiResponse(description='Cloud Storage status'), + #TODO + '404': OpenApiResponse(description=''), }, - tags=['cloud storages'] + tags=['cloud storages'], + versions=['v1'], ) @action(detail=True, methods=['GET'], url_path='status') def status(self, request, pk): @@ -1402,15 +1748,6 @@ def rq_handler(job, exc_type, exc_value, tb): return True -# TODO: Method should be reimplemented as a separated view -# @swagger_auto_schema(method='put', manual_parameters=[openapi.Parameter('format', in_=openapi.IN_QUERY, -# description='A name of a loader\nYou can get annotation loaders from this API:\n/server/annotation/formats', -# required=True, type=openapi.TYPE_STRING)], -# operation_summary='Method allows to upload annotations', -# responses={'202': openapi.Response(description='Load of annotations has been started'), -# '201': openapi.Response(description='Annotations have been uploaded')}, -# tags=['tasks']) -# @api_view(['PUT']) def _import_annotations(request, rq_id, rq_func, pk, format_name): format_desc = {f.DISPLAY_NAME: f for f in dm.views.get_import_formats()}.get(format_name) diff --git a/cvat/apps/iam/urls.py b/cvat/apps/iam/urls.py index 76b0d3192fc..cfc3c441a4f 100644 --- a/cvat/apps/iam/urls.py +++ b/cvat/apps/iam/urls.py @@ -38,4 +38,4 @@ name='account_email_verification_sent'), ] -urlpatterns = [path('auth/', include(urlpatterns))] \ No newline at end of file +urlpatterns = [path('auth/', include((urlpatterns, 'cvat'), namespace='v1'))] \ No newline at end of file diff --git a/cvat/apps/iam/views.py b/cvat/apps/iam/views.py index df986457207..88d3574756e 100644 --- a/cvat/apps/iam/views.py +++ b/cvat/apps/iam/views.py @@ -13,8 +13,8 @@ from furl import furl from django.utils.decorators import method_decorator -from drf_yasg.utils import swagger_auto_schema -from drf_yasg import openapi + + from .authentication import Signer @@ -73,18 +73,18 @@ def __call__(self, request): return self.get_response(request) -@method_decorator(name='post', decorator=swagger_auto_schema( - request_body=openapi.Schema( - type=openapi.TYPE_OBJECT, - required=[ - 'url' - ], - properties={ - 'url': openapi.Schema(type=openapi.TYPE_STRING) - } - ), - responses={'200': openapi.Response(description='text URL')} -)) +# @method_decorator(name='post', decorator=swagger_auto_schema( +# request_body=openapi.Schema( +# type=openapi.TYPE_OBJECT, +# required=[ +# 'url' +# ], +# properties={ +# 'url': openapi.Schema(type=openapi.TYPE_STRING) +# } +# ), +# responses={'200': openapi.Response(description='text URL')} +# )) class SigningView(views.APIView): """ This method signs URL for access to the server. diff --git a/cvat/apps/restrictions/views.py b/cvat/apps/restrictions/views.py index 46af4917b8a..c446f3e593d 100644 --- a/cvat/apps/restrictions/views.py +++ b/cvat/apps/restrictions/views.py @@ -8,7 +8,7 @@ from rest_framework import viewsets from rest_framework.permissions import AllowAny from rest_framework.renderers import TemplateHTMLRenderer -from drf_yasg.utils import swagger_auto_schema + from cvat.apps.restrictions.serializers import UserAgreementSerializer @@ -23,10 +23,10 @@ def get_serializer(self, *args, **kwargs): pass @staticmethod - @swagger_auto_schema( - method='get', - operation_summary='Method provides user agreements that the user must accept to register', - responses={'200': UserAgreementSerializer}) + # @swagger_auto_schema( + # method='get', + # operation_summary='Method provides user agreements that the user must accept to register', + # responses={'200': UserAgreementSerializer}) @action(detail=False, methods=['GET'], serializer_class=UserAgreementSerializer, url_path='user-agreements') def user_agreements(request): user_agreements = settings.RESTRICTIONS['user_agreements'] diff --git a/cvat/apps/training/views.py b/cvat/apps/training/views.py index acc75463fa1..6a4e86f347a 100644 --- a/cvat/apps/training/views.py +++ b/cvat/apps/training/views.py @@ -1,5 +1,5 @@ from cacheops import cache, CacheMiss -from drf_yasg.utils import swagger_auto_schema + from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated @@ -15,7 +15,7 @@ def get_permissions(self): return [perm() for perm in permissions] - @swagger_auto_schema(method='get', operation_summary='Returns prediction for image') + # @swagger_auto_schema(method='get', operation_summary='Returns prediction for image') @action(detail=False, methods=['GET'], url_path='frame') def predict_image(self, request): frame = self.request.query_params.get('frame') @@ -37,8 +37,8 @@ def predict_image(self, request): return Response(resp) - @swagger_auto_schema(method='get', - operation_summary='Returns information of the tasks of the project with the selected id') + # @swagger_auto_schema(method='get', + # operation_summary='Returns information of the tasks of the project with the selected id') @action(detail=False, methods=['GET'], url_path='status') def predict_status(self, request): project_id = self.request.query_params.get('project') diff --git a/cvat/requirements/base.txt b/cvat/requirements/base.txt index a201a71467c..9e88c051d16 100644 --- a/cvat/requirements/base.txt +++ b/cvat/requirements/base.txt @@ -30,7 +30,7 @@ django-filter==2.3.0 Markdown==3.2.2 djangorestframework==3.12.4 Pygments==2.7.4 -drf-yasg==1.20.0 +drf-spectacular==0.21.1 Shapely==1.7.1 pdf2image==1.14.0 django-rest-auth[with_social]==0.9.5 diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 373f884d24c..d441afe6cbb 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -113,7 +113,7 @@ def add_ssh_keys(): 'rest_framework', 'rest_framework.authtoken', 'django_filters', - 'drf_yasg', + 'drf_spectacular', 'rest_auth', 'django.contrib.sites', 'allauth', @@ -176,6 +176,7 @@ def add_ssh_keys(): 'anon': '100/minute', }, 'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata', + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', } REST_AUTH_REGISTER_SERIALIZERS = { @@ -515,3 +516,17 @@ def add_ssh_keys(): TUS_MAX_FILE_SIZE = 26843545600 # 25gb TUS_DEFAULT_CHUNK_SIZE = 104857600 # 100 mb + +SPECTACULAR_SETTINGS = { + 'TITLE': 'CVAT REST API', + 'DESCRIPTION': 'REST API for Computer Vision Annotation Tool (CVAT)', + 'VERSION': 'v1', + 'CONTACT': { + 'email': 'nikita.manovich@intel.com', + }, + 'LICENSE': { + 'name': 'MIT License', + }, + # OTHER SETTINGS + # https://drf-spectacular.readthedocs.io/en/latest/settings.html +} \ No newline at end of file From 8a1dd902d4df5b36f60b0e81640d111460be31ce Mon Sep 17 00:00:00 2001 From: Maya Date: Thu, 20 Jan 2022 13:52:51 +0300 Subject: [PATCH 02/22] Add examples --- cvat/apps/engine/serializers.py | 69 ++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index f2a87f071aa..3e008e29c99 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -17,6 +17,8 @@ from cvat.apps.engine.log import slogger from cvat.apps.engine.utils import parse_specific_attributes +from drf_spectacular.utils import OpenApiExample, extend_schema_serializer + class BasicUserSerializer(serializers.ModelSerializer): def validate(self, data): if hasattr(self, 'initial_data'): @@ -861,7 +863,7 @@ def to_internal_value(self, data): def to_representation(self, instance): return instance.filename if instance else instance -class BaseCloudStorageSerializer(serializers.ModelSerializer): +class ReadCloudStorageSerializer(serializers.ModelSerializer): owner = BasicUserSerializer(required=False) manifests = ManifestSerializer(many=True, default=[]) class Meta: @@ -869,7 +871,70 @@ class Meta: exclude = ['credentials'] read_only_fields = ('created_date', 'updated_date', 'owner', 'organization') -class CloudStorageSerializer(serializers.ModelSerializer): +@extend_schema_serializer( + examples=[ + OpenApiExample( + 'Create AWS S3 cloud storage with credentials', + description='', + value={ + 'provider_type': models.CloudProviderChoice.AWS_S3, + 'resource': 'somebucket', + 'display_name': 'Bucket', + 'credentials_type': models.CredentialsTypeChoice.KEY_SECRET_KEY_PAIR, + 'specific_attributes': 'region=eu-central-1', + 'description': 'Some description', + 'manifests': [ + 'manifest.jsonl' + ], + + }, + request_only=True, + ), + OpenApiExample( + 'Create AWS S3 cloud storage without credentials', + value={ + 'provider_type': models.CloudProviderChoice.AWS_S3, + 'resource': 'somebucket', + 'display_name': 'Bucket', + 'credentials_type': models.CredentialsTypeChoice.ANONYMOUS_ACCESS, + 'manifests': [ + 'manifest.jsonl' + ], + }, + request_only=True, + ), + OpenApiExample( + 'Create Azure cloud storage', + value={ + 'provider_type': models.CloudProviderChoice.AZURE_CONTAINER, + 'resource': 'sonecontainer', + 'display_name': 'Container', + 'credentials_type': models.CredentialsTypeChoice.ACCOUNT_NAME_TOKEN_PAIR, + 'account_name': 'someaccount', + 'session_token': 'xxx', + 'manifests': [ + 'manifest.jsonl' + ], + }, + request_only=True, + ), + OpenApiExample( + 'Create GCS', + value={ + 'provider_type': models.CloudProviderChoice.GOOGLE_CLOUD_STORAGE, + 'resource': 'somebucket', + 'display_name': 'Bucket', + 'credentials_type': models.CredentialsTypeChoice.KEY_FILE_PATH, + 'key_file': 'file', + 'manifests': [ + 'manifest.jsonl' + ], + }, + request_only=True, + ) + ] +) +class WriteCloudStorageSerializer(serializers.ModelSerializer): owner = BasicUserSerializer(required=False) session_token = serializers.CharField(max_length=440, allow_blank=True, required=False) key = serializers.CharField(max_length=20, allow_blank=True, required=False) From d6cc1d7fffe182e5040077e02832b922b8138dd9 Mon Sep 17 00:00:00 2001 From: Maya Date: Thu, 20 Jan 2022 13:55:14 +0300 Subject: [PATCH 03/22] Update engine documentation --- cvat/apps/engine/views.py | 992 ++++++++++++++++---------------------- 1 file changed, 419 insertions(+), 573 deletions(-) diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 0fda9d67877..9048abaa671 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -6,6 +6,7 @@ import io import os import os.path as osp +from pydoc import resolve import pytz import shutil import traceback @@ -22,13 +23,12 @@ from django.db import IntegrityError from django.http import HttpResponse, HttpResponseNotFound, HttpResponseBadRequest from django.utils import timezone -from django.utils.decorators import method_decorator from django_filters import rest_framework as filters from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import ( - OpenApiExample, OpenApiParameter, OpenApiResponse, extend_schema, + OpenApiExample, OpenApiParameter, OpenApiResponse, extend_schema, PolymorphicProxySerializer, extend_schema_view ) @@ -61,8 +61,8 @@ FileInfoSerializer, JobReadSerializer, JobWriteSerializer, LabeledDataSerializer, LogEventSerializer, ProjectSerializer, ProjectSearchSerializer, RqStatusSerializer, TaskSerializer, UserSerializer, PluginsSerializer, IssueReadSerializer, - IssueWriteSerializer, CommentReadSerializer, CommentWriteSerializer, CloudStorageSerializer, - BaseCloudStorageSerializer, DatasetFileSerializer) + IssueWriteSerializer, CommentReadSerializer, CommentWriteSerializer, WriteCloudStorageSerializer, + ReadCloudStorageSerializer, DatasetFileSerializer) from utils.dataset_manifest import ImageManifestManager from cvat.apps.engine.utils import av_scan_paths @@ -84,14 +84,11 @@ def get_serializer(self, *args, **kwargs): pass @staticmethod - @extend_schema( - summary='Method provides basic CVAT information', + @extend_schema(summary='Method provides basic CVAT information', responses={ - '200': AboutSerializer + '200': AboutSerializer, }, - tags=['server'], - versions=['v1'], - ) + tags=['server'], versions=['v1']) @action(detail=False, methods=['GET'], serializer_class=AboutSerializer) def about(request): from cvat import __version__ as cvat_version @@ -111,17 +108,14 @@ def about(request): return Response(data=serializer.data) @staticmethod - @extend_schema( - request=ExceptionSerializer, - tags=['server'], - versions=['v1'], - ) + @extend_schema(summary='Method saves an exception from a client on the server', + description='Sends logs to the ELK if it is connected', + request=ExceptionSerializer, responses={ + '201': ExceptionSerializer, + }, + tags=['server'], versions=['v1']) @action(detail=False, methods=['POST'], serializer_class=ExceptionSerializer) def exception(request): - """ - Saves an exception from a client on the server - Sends logs to the ELK if it is connected - """ serializer = ExceptionSerializer(data=request.data) if serializer.is_valid(raise_exception=True): additional_info = { @@ -141,18 +135,14 @@ def exception(request): return Response(serializer.data, status=status.HTTP_201_CREATED) @staticmethod - @extend_schema( - methods=['POST'], + @extend_schema(summary='Method saves logs from a client on the server', + description='Sends logs to the ELK if it is connected', request=LogEventSerializer(many=True), - tags=['server'], - versions=['v1'], - ) + responses={ + '201': LogEventSerializer(many=True), + }, tags=['server'], versions=['v1']) @action(detail=False, methods=['POST'], serializer_class=LogEventSerializer) def logs(request): - """ - Saves logs from a client on the server - Sends logs to the ELK if it is connected - """ serializer = LogEventSerializer(many=True, data=request.data) if serializer.is_valid(raise_exception=True): user = { "username": request.user.username } @@ -170,22 +160,14 @@ def logs(request): @staticmethod @extend_schema( - methods=['GET'], summary='Returns all files and folders that are on the server along specified path', parameters=[ - OpenApiParameter( - 'directory', - description='Directory to browse', - location=OpenApiParameter.QUERY, - type=OpenApiTypes.STR, - ) + OpenApiParameter('directory', description='Directory to browse', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR) ], responses={ '200' : FileInfoSerializer(many=True) - }, - tags=['server'], - versions=['v1'], - ) + }, tags=['server'], versions=['v1']) @action(detail=False, methods=['GET'], serializer_class=FileInfoSerializer) def share(request): param = request.query_params.get('directory', '/') @@ -215,14 +197,10 @@ def share(request): @staticmethod @extend_schema( - methods=['GET'], summary='Method provides the list of supported annotations formats', responses={ - '200': DatasetFormatsSerializer(), - }, - tags=['server'], - versions=['v1'], - ) + '200': DatasetFormatsSerializer, + }, tags=['server'], versions=['v1']) @action(detail=False, methods=['GET'], url_path='annotation/formats') def annotation_formats(request): data = dm.views.get_all_formats() @@ -230,14 +208,10 @@ def annotation_formats(request): @staticmethod @extend_schema( - methods=['GET'], summary='Method provides allowed plugins', responses={ - '200': PluginsSerializer(), - }, - tags=['server'], - versions=['v1'], - ) + '200': PluginsSerializer, + }, tags=['server'], versions=['v1']) @action(detail=False, methods=['GET'], url_path='plugins', serializer_class=PluginsSerializer) def plugins(request): response = { @@ -263,84 +237,46 @@ class Meta: model = models.Project fields = ("id", "name", "owner", "status") -@method_decorator(name='list', decorator=extend_schema( +@extend_schema_view(list=extend_schema( summary='Returns a paginated list of projects according to query parameters (12 projects per page)', parameters=[ - OpenApiParameter( - 'id', - location=OpenApiParameter.QUERY, - description='A unique number value identifying this project', - type=OpenApiTypes.NUMBER - ), - OpenApiParameter( - 'name', - location=OpenApiParameter.QUERY, - description='Find all projects where name contains a parameter value', - type=OpenApiTypes.STR - ), - OpenApiParameter( - 'owner', - location=OpenApiParameter.QUERY, - description='Find all project where owner name contains a parameter value', - type=OpenApiTypes.STR - ), - OpenApiParameter( - 'status', - location=OpenApiParameter.QUERY, - description='Find all projects with a specific status', - type=OpenApiTypes.STR, - enum=StatusChoice.list() - ), - OpenApiParameter( - 'names_only', - location=OpenApiParameter.QUERY, - description="Returns only names and id's of projects", - type=OpenApiTypes.BOOL - ) + OpenApiParameter('id', description='A unique number value identifying this project', + location=OpenApiParameter.QUERY, type=OpenApiTypes.NUMBER), + OpenApiParameter('name', description='Find all projects where name contains a parameter value', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR), + OpenApiParameter('owner', description='Find all project where owner name contains a parameter value', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR), + OpenApiParameter('status', description='Find all projects with a specific status', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, enum=StatusChoice.list()), + OpenApiParameter('names_only', description="Returns only names and id's of projects", + location=OpenApiParameter.QUERY, type=OpenApiTypes.BOOL) ], - #TODO responses={ - '200': OpenApiResponse(description=''), - }, - tags=['projects'], - versions=['v1'], -)) -@method_decorator(name='create', decorator=extend_schema( + '200': PolymorphicProxySerializer(component_name='PolymorphicProject', + serializers=[ + ProjectSerializer, ProjectSearchSerializer, + ], resource_type_field_name='type', many=True), + }, tags=['projects'], versions=['v1'])) +@extend_schema_view(create=extend_schema( summary='Method creates a new project', - #TODO responses={ - '200': OpenApiResponse(description=''), - }, - tags=['projects'], - versions=['v1'], -)) -@method_decorator(name='retrieve', decorator=extend_schema( + '201': ProjectSerializer, + }, tags=['projects'], versions=['v1'])) +@extend_schema_view(retrieve=extend_schema( summary='Method returns details of a specific project', - #TODO responses={ - '200': OpenApiResponse(description=''), - }, - tags=['projects'], - versions=['v1'], -)) -@method_decorator(name='destroy', decorator=extend_schema( + '200': ProjectSerializer, + }, tags=['projects'], versions=['v1'])) +@extend_schema_view(destroy=extend_schema( summary='Method deletes a specific project', - #TODO responses={ - '204': OpenApiResponse(description=''), - }, - tags=['projects'], - versions=['v1'], -)) -@method_decorator(name='partial_update', decorator=extend_schema( + '204': OpenApiResponse(description='The project has been deleted'), + }, tags=['projects'], versions=['v1'])) +@extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in a project', - #TODO responses={ - '200': OpenApiResponse(description=''), - }, - tags=['projects'], - versions=['v1'], -)) + '200': ProjectSerializer, + }, tags=['projects'], versions=['v1'])) class ProjectViewSet(viewsets.ModelViewSet): queryset = models.Project.objects.prefetch_related(Prefetch('label_set', queryset=models.Label.objects.order_by('id') @@ -372,14 +308,10 @@ def perform_create(self, serializer): organization=self.request.iam_context['organization']) @extend_schema( - methods=['GET'], - summary='Returns information of the tasks of the project with the selected id', + summary='Method returns information of the tasks of the project with the selected id', responses={ - '200': TaskSerializer(many=True) - }, - tags=['projects'], - versions=['v1'], - ) + '200': TaskSerializer(many=True), + }, tags=['projects'], versions=['v1']) @action(detail=True, methods=['GET'], serializer_class=TaskSerializer) def tasks(self, request, pk): self.get_object() # force to call check_object_permissions @@ -396,62 +328,33 @@ def tasks(self, request, pk): return Response(serializer.data) - @extend_schema( - methods=['GET'], - summary='Export project as a dataset in a specific format', + @extend_schema(methods=['GET'], summary='Export project as a dataset in a specific format', parameters=[ - OpenApiParameter( - 'format', - location=OpenApiParameter.QUERY, - description='Desired output format name\nYou can get the list of supported formats at:\n/server/annotation/formats', - type=OpenApiTypes.STR, - required=True - ), - OpenApiParameter( - 'filename', - location=OpenApiParameter.QUERY, - description='Desired output file name', - type=OpenApiTypes.STR, - required=False - ), - OpenApiParameter( - 'action', - location=OpenApiParameter.QUERY, - description='Used to start downloading process after annotation file had been created', - type=OpenApiTypes.STR, - required=False, - enum=['download', 'import_status'] - ) + OpenApiParameter('format', description='Desired output format name\n' + 'You can get the list of supported formats at:\n/server/annotation/formats', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=True), + OpenApiParameter('filename', description='Desired output file name', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=False), + OpenApiParameter('action', description='Used to start downloading process after annotation file had been created', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=False, enum=['download', 'import_status']) ], responses={ - '202': OpenApiResponse(description='Exporting has been started'), - '201': OpenApiResponse(description='Output file is ready for downloading'), '200': OpenApiResponse(description='Download of file started'), + '201': OpenApiResponse(description='Output file is ready for downloading'), + '202': OpenApiResponse(description='Exporting has been started'), '405': OpenApiResponse(description='Format is not available'), - }, - tags=['projects'], - versions=['v1'], - ) - @extend_schema( - methods=['POST'], - summary='Import dataset in specific format as a project', + }, tags=['projects'], versions=['v1']) + @extend_schema(methods=['POST'], summary='Import dataset in specific format as a project', parameters=[ - OpenApiParameter( - 'format', - location=OpenApiParameter.QUERY, - description='Desired dataset format name\nYou can get the list of supported formats at:\n/server/annotation/formats', - type=OpenApiTypes.STR, - required=True - ) + OpenApiParameter('format', description='Desired dataset format name\n' + 'You can get the list of supported formats at:\n/server/annotation/formats', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=True) ], responses={ '202': OpenApiResponse(description='Exporting has been started'), '400': OpenApiResponse(description='Failed to import dataset'), '405': OpenApiResponse(description='Format is not available'), - }, - tags=['projects'], - versions=['v1'], - ) + }, tags=['projects'], versions=['v1']) @action(detail=True, methods=['GET', 'POST'], serializer_class=None, url_path='dataset') def dataset(self, request, pk): @@ -504,43 +407,23 @@ def dataset(self, request, pk): filename=request.query_params.get("filename", "").lower(), ) - @extend_schema( - methods=['GET'], - summary='Method allows to download project annotations', + @extend_schema(summary='Method allows to download project annotations', parameters=[ - OpenApiParameter( - 'format', - location=OpenApiParameter.QUERY, - description='Desired output format name\nYou can get the list of supported formats at:\n/server/annotation/formats', - type=OpenApiTypes.STR, - required=True - ), - OpenApiParameter( - 'filename', - location=OpenApiParameter.QUERY, - description='Desired output file name', - type=OpenApiTypes.STR, - required=False - ), - OpenApiParameter( - 'action', - location=OpenApiParameter.QUERY, - description='Used to start downloading process after annotation file had been created', - type=OpenApiTypes.STR, - required=False, - enum=['download'] - ) + OpenApiParameter('format', description='Desired output format name\n' + 'You can get the list of supported formats at:\n/server/annotation/formats', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=True), + OpenApiParameter('filename', description='Desired output file name', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=False), + OpenApiParameter('action', description='Used to start downloading process after annotation file had been created', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=False, enum=['download']) ], responses={ - '202': OpenApiResponse(description='Dump of annotations has been started'), - '201': OpenApiResponse(description='Annotations file is ready to download'), '200': OpenApiResponse(description='Download of file started'), - '405': OpenApiResponse(description='Format is not available'), + '201': OpenApiResponse(description='Annotations file is ready to download'), + '202': OpenApiResponse(description='Dump of annotations has been started'), '401': OpenApiResponse(description='Format is not specified'), - }, - tags=['projects'], - versions=['v1'], - ) + '405': OpenApiResponse(description='Format is not available'), + }, tags=['projects'], versions=['v1']) @action(detail=True, methods=['GET'], serializer_class=LabeledDataSerializer) def annotations(self, request, pk): @@ -558,11 +441,22 @@ def annotations(self, request, pk): else: return Response("Format is not specified",status=status.HTTP_400_BAD_REQUEST) + @extend_schema(summary='Methods creates a backup copy of a project', + responses={ + '200': OpenApiResponse(description='Download of file started'), + '201': OpenApiResponse(description='Output backup file is ready for downloading'), + '202': OpenApiResponse(description='Creating a backup file has been started'), + }, tags=['projects'], versions=['v1']) @action(methods=['GET'], detail=True, url_path='backup') def export_backup(self, request, pk=None): db_project = self.get_object() # force to call check_object_permissions return backup.export(db_project, request) + @extend_schema(summary='Methods create a project from a backup', + responses={ + '201': OpenApiResponse(description='The project has been imported'), + '202': OpenApiResponse(description='Importing a backup file has been started'), + }, tags=['projects'], versions=['v1']) @action(detail=False, methods=['POST'], url_path='backup') def import_backup(self, request, pk=None): return backup.import_project(request) @@ -673,105 +567,48 @@ class Meta: fields = ("id", "project_id", "project", "name", "owner", "mode", "status", "assignee") -# class DjangoFilterInspector(CoreAPICompatInspector): -# def get_filter_parameters(self, filter_backend): -# if isinstance(filter_backend, DjangoFilterBackend): -# result = super(DjangoFilterInspector, self).get_filter_parameters(filter_backend) -# res = result.copy() - -# for param in result: -# if param.get('name') == 'project_id' or param.get('name') == 'project': -# res.remove(param) -# return res - -# return NotHandled - -@method_decorator(name='list', decorator=extend_schema( +@extend_schema_view(list=extend_schema( summary='Returns a paginated list of tasks according to query parameters (10 tasks per page)', parameters=[ - OpenApiParameter( - 'id', - location=OpenApiParameter.QUERY, - description='A unique number value identifying this task', - type=OpenApiTypes.NUMBER - ), - OpenApiParameter( - 'name', - location=OpenApiParameter.QUERY, - description='Find all tasks where name contains a parameter value', - type=OpenApiTypes.STR - ), - OpenApiParameter( - 'owner', - location=OpenApiParameter.QUERY, - description='Find all tasks where owner name contains a parameter value', - type=OpenApiTypes.STR - ), - OpenApiParameter( - 'mode', - location=OpenApiParameter.QUERY, - description='Find all tasks with a specific mode', - type=OpenApiTypes.STR, - enum=['annotation', 'interpolation'] - ), - OpenApiParameter( - 'status', - location=OpenApiParameter.QUERY, - description='Find all tasks with a specific status', - type=OpenApiTypes.STR, - enum=StatusChoice.list() - ), - OpenApiParameter( - 'assignee', - location=OpenApiParameter.QUERY, - description='Find all tasks where assignee name contains a parameter value', - type=OpenApiTypes.STR - ) - ], - #filter_inspectors=[DjangoFilterInspector] - tags=['tasks'], - versions=['v1'], -)) -@method_decorator(name='create', decorator=extend_schema( + OpenApiParameter('id', description='A unique number value identifying this task', + location=OpenApiParameter.QUERY, type=OpenApiTypes.NUMBER), + OpenApiParameter('name', description='Find all tasks where name contains a parameter value', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR), + OpenApiParameter('owner', description='Find all tasks where owner name contains a parameter value', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR), + OpenApiParameter('mode', description='Find all tasks with a specific mode', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, enum=['annotation', 'interpolation']), + OpenApiParameter('status', description='Find all tasks with a specific status', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, enum=StatusChoice.list()), + OpenApiParameter('assignee', description='Find all tasks where assignee name contains a parameter value', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR) + ], + responses={ + '200': TaskSerializer(many=True), + }, tags=['tasks'], versions=['v1'])) +@extend_schema_view(create=extend_schema( summary='Method creates a new task in a database without any attached images and videos', responses={ - '202': OpenApiResponse(description=''), - '201': OpenApiResponse(description=''), - '200': OpenApiResponse(description=''), - }, - tags=['tasks'], - versions=['v1'], -)) -@method_decorator(name='retrieve', decorator=extend_schema( + '201': OpenApiResponse(description='The task has been created'), + }, tags=['tasks'], versions=['v1'])) +@extend_schema_view(retrieve=extend_schema( summary='Method returns details of a specific task', - responses=OpenApiResponse(description=''), - tags=['tasks'], - versions=['v1'], -)) -@method_decorator(name='update', decorator=extend_schema( + responses=TaskSerializer, tags=['tasks'], versions=['v1'])) +@extend_schema_view(update=extend_schema( summary='Method updates a task by id', responses={ - '200':OpenApiResponse(description=''), # or serializer - }, - tags=['tasks'], - versions=['v1'], -)) -@method_decorator(name='destroy', decorator=extend_schema( + '200': TaskSerializer, + }, tags=['tasks'], versions=['v1'])) +@extend_schema_view(destroy=extend_schema( summary='Method deletes a specific task, all attached jobs, annotations, and data', responses={ '204': OpenApiResponse('The task has been deleted'), - }, - tags=['tasks'], - versions=['v1'], -)) -@method_decorator(name='partial_update', decorator=extend_schema( + }, tags=['tasks'], versions=['v1'])) +@extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in a task', responses={ - '200': OpenApiResponse(description=''), # or serializer - }, - tags=['tasks'], - versions=['v1'], -)) + '200': TaskSerializer, + }, tags=['tasks'], versions=['v1'])) class TaskViewSet(UploadMixin, viewsets.ModelViewSet): queryset = Task.objects.prefetch_related( Prefetch('label_set', queryset=models.Label.objects.order_by('id')), @@ -791,36 +628,21 @@ def get_queryset(self): return queryset - @extend_schema( - methods=['POST'], - summary='Method recreates a task from an attached task backup file', - #TODO + @extend_schema(summary='Method recreates a task from an attached task backup file', responses={ - '202': OpenApiResponse(description=''), - '201': OpenApiResponse(description=''), - '400': OpenApiResponse(description=''), - '500': OpenApiResponse(description=''), - }, - tags=['tasks'], - versions=['v1'], - ) + '201': OpenApiResponse(description='The task has been imported'), + '202': OpenApiResponse(description='Importing a backup file has been started'), + }, tags=['tasks'], versions=['v1']) @action(detail=False, methods=['POST'], url_path='backup') def import_backup(self, request, pk=None): return backup.import_task(request) - @extend_schema( - methods=['GET'], - summary='Method backup a specified task', - #TODO + @extend_schema(summary='Method backup a specified task', responses={ - '202': OpenApiResponse(description=''), - '201': OpenApiResponse(description=''), - '400': OpenApiResponse(description=''), - '500': OpenApiResponse(description=''), - }, - tags=['tasks'], - versions=['v1'], - ) + '200': OpenApiResponse(description='Download of file started'), + '201': OpenApiResponse(description='Output backup file is ready for downloading'), + '202': OpenApiResponse(description='Creating a backup file has been started'), + }, tags=['tasks'], versions=['v1']) @action(methods=['GET'], detail=True, url_path='backup') def export_backup(self, request, pk=None): db_task = self.get_object() # force to call check_object_permissions @@ -852,15 +674,10 @@ def perform_destroy(self, instance): db_project = instance.project db_project.save() - @extend_schema( - methods=['GET'], - summary='Returns a list of jobs for a specific task', + @extend_schema(summary='Method returns a list of jobs for a specific task', responses={ '200': JobReadSerializer(many=True), - }, - tags=['tasks'], - versions=['v1'], - ) + }, tags=['tasks'], versions=['v1']) @action(detail=True, methods=['GET'], serializer_class=JobReadSerializer) def jobs(self, request, pk): self.get_object() # force to call check_object_permissions @@ -904,39 +721,34 @@ def upload_finished(self, request): task.create(db_task.id, data) return Response(serializer.data, status=status.HTTP_202_ACCEPTED) - @extend_schema( - methods='post', + @extend_schema(methods=['POST'], summary='Method permanently attaches images or video to a task. Supports tus uploads, see more https://tus.io/', request=DataSerializer, parameters=[ - OpenApiParameter('Upload-Start', location=OpenApiParameter.HEADER, type=OpenApiTypes.BOOL, - description='Initializes data upload. No data should be sent with this header'), - OpenApiParameter('Upload-Multiple', location=OpenApiParameter.HEADER, type=OpenApiTypes.BOOL, - description='Indicates that data with this request are single or multiple files that should be attached to a task'), - OpenApiParameter('Upload-Finish', location=OpenApiParameter.HEADER, type=OpenApiTypes.BOOL, - description='Finishes data upload. Can be combined with Upload-Start header to create task data with one request'), - ], - #TODO - responses={}, - tags=['tasks'], - versions=['v1'], - ) - @extend_schema(methods=['OPTIONS', 'POST', 'GET'], summary='Method returns data for a specific task', + OpenApiParameter('Upload-Start', location=OpenApiParameter.HEADER, type=OpenApiTypes.BOOL, + description='Initializes data upload. No data should be sent with this header'), + OpenApiParameter('Upload-Multiple', location=OpenApiParameter.HEADER, type=OpenApiTypes.BOOL, + description='Indicates that data with this request are single or multiple files that should be attached to a task'), + OpenApiParameter('Upload-Finish', location=OpenApiParameter.HEADER, type=OpenApiTypes.BOOL, + description='Finishes data upload. Can be combined with Upload-Start header to create task data with one request'), + ], + responses={ + '202': OpenApiResponse(), + }, tags=['tasks'], versions=['v1']) + @extend_schema(methods=['GET'], summary='Method returns data for a specific task', parameters=[ - OpenApiParameter('type', location=OpenApiParameter.QUERY, required=True, type=OpenApiTypes.STR, - enum=['chunk', 'frame', 'preview', 'context_image'], + OpenApiParameter('type', location=OpenApiParameter.QUERY, required=True, + type=OpenApiTypes.STR, enum=['chunk', 'frame', 'preview', 'context_image'], description='Specifies the type of the requested data'), - OpenApiParameter('quality', location=OpenApiParameter.QUERY, required=True, type=OpenApiTypes.STR, - enum=['compressed', 'original'], + OpenApiParameter('quality', location=OpenApiParameter.QUERY, required=True, + type=OpenApiTypes.STR, enum=['compressed', 'original'], description="Specifies the quality level of the requested data, doesn't matter for 'preview' type"), OpenApiParameter('number', location=OpenApiParameter.QUERY, required=True, type=OpenApiTypes.NUMBER, description="A unique number value identifying chunk or frame, doesn't matter for 'preview' type"), - ], - #TODO - responses={}, - tags=['tasks'], - versions=['v1'], - ) + ], + responses={ + '200': OpenApiResponse('Data of a specific type'), + }, tags=['tasks'], versions=['v1']) @action(detail=True, methods=['OPTIONS', 'POST', 'GET'], url_path=r'data/?$') def data(self, request, pk): db_task = self.get_object() # call check_object_permissions as well @@ -965,52 +777,39 @@ def data(self, request, pk): @extend_schema(methods=['GET'], summary='Method allows to download task annotations', parameters=[ - OpenApiParameter('format', location=OpenApiParameter.QUERY, - description="Desired output format name\nYou can get the list of supported formats at:\n/server/annotation/formats", - type=OpenApiTypes.STR, required=False), - OpenApiParameter('filename', location=OpenApiParameter.QUERY, - description="Desired output file name", - type=OpenApiTypes.STR, required=False), + OpenApiParameter('format', location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=False, + description="Desired output format name\nYou can get the list of supported formats at:\n/server/annotation/formats"), + OpenApiParameter('filename', description='Desired output file name', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=False), OpenApiParameter('action', location=OpenApiParameter.QUERY, description='Used to start downloading process after annotation file had been created', type=OpenApiTypes.STR, required=False, enum=['download']) ], responses={ - '202': OpenApiResponse(description='Dump of annotations has been started'), - '201': OpenApiResponse(description='Annotations file is ready to download'), '200': OpenApiResponse(description='Download of file started'), + '201': OpenApiResponse(description='Annotations file is ready to download'), + '202': OpenApiResponse(description='Dump of annotations has been started'), '405': OpenApiResponse(description='Format is not available'), - }, - tags=['tasks'], - versions=['v1'], - ) + }, tags=['tasks'], versions=['v1']) @extend_schema(methods=['PUT'], summary='Method allows to upload task annotations', parameters=[ - OpenApiParameter('format', location=OpenApiParameter.QUERY, - description='Input format name\nYou can get the list of supported formats at:\n/server/annotation/formats', - type=OpenApiTypes.STR, required=False), + OpenApiParameter('format', location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=False, + description='Input format name\nYou can get the list of supported formats at:\n/server/annotation/formats'), ], responses={ - '202': OpenApiResponse(description='Uploading has been started'), '201': OpenApiResponse(description='Uploading has finished'), + '202': OpenApiResponse(description='Uploading has been started'), '405': OpenApiResponse(description='Format is not available'), - }, - tags=['tasks'], - versions=['v1'], - ) + }, tags=['tasks'], versions=['v1']) @extend_schema(methods=['PATCH'], summary='Method performs a partial update of annotations in a specific task', - parameters=[OpenApiParameter('action', location=OpenApiParameter.QUERY, required=True, type=OpenApiTypes.STR, - enum=['create', 'update', 'delete'])], - tags=['tasks'], - versions=['v1'], - ) + parameters=[ + OpenApiParameter('action', location=OpenApiParameter.QUERY, required=True, + type=OpenApiTypes.STR, enum=['create', 'update', 'delete']), + ], tags=['tasks'], versions=['v1']) @extend_schema(methods=['DELETE'], summary='Method deletes all annotations for a specific task', responses={ '204': OpenApiResponse(description='The annotation has been deleted'), - }, - tags=['tasks'], - versions=['v1'], - ) + }, tags=['tasks'], versions=['v1']) @action(detail=True, methods=['GET', 'DELETE', 'PUT', 'PATCH'], serializer_class=LabeledDataSerializer) def annotations(self, request, pk): @@ -1063,15 +862,10 @@ def annotations(self, request, pk): return Response(data) @extend_schema( - methods=['GET'], summary='When task is being created the method returns information about a status of the creation process', - #TODO responses={ - '200': OpenApiResponse(description=''), - }, - tags=['tasks'], - versions=['v1'], - ) + '200': RqStatusSerializer, + }, tags=['tasks'], versions=['v1']) @action(detail=True, methods=['GET'], serializer_class=RqStatusSerializer) def status(self, request, pk): self.get_object() # force to call check_object_permissions @@ -1101,8 +895,10 @@ def _get_rq_response(queue, job_id): return response @staticmethod - # @extend_schema(methods=['GET'], summary='Method provides a meta information about media files which are related with the task', - # responses={'200': DataMetaSerializer()}) + @extend_schema(summary='Method provides a meta information about media files which are related with the task', + responses={ + '200': DataMetaSerializer, + }, tags=['tasks'], versions=['v1']) @action(detail=True, methods=['GET'], serializer_class=DataMetaSerializer, url_path='data/meta') def data_info(request, pk): @@ -1130,24 +926,23 @@ def data_info(request, pk): serializer = DataMetaSerializer(db_data) return Response(serializer.data) - # @extend_schema(methods=['GET'], summary='Export task as a dataset in a specific format', - # parameters=[ - # OpenApiParameter('format', OpenApiParameter.QUERY, - # description="Desired output format name\nYou can get the list of supported formats at:\n/server/annotation/formats", - # type=OpenApiTypes.STR, required=True), - # OpenApiParameter('filename', OpenApiParameter.QUERY, - # description="Desired output file name", - # type=OpenApiTypes.STR, required=False), - # OpenApiParameter('action', location=OpenApiParameter.QUERY, - # description='Used to start downloading process after annotation file had been created', - # type=OpenApiTypes.STR, required=False, enum=['download']) - # ], - # responses={'202': OpenApiResponse(description='Exporting has been started'), - # '201': OpenApiResponse(description='Output file is ready for downloading'), - # '200': OpenApiResponse(description='Download of file started'), - # '405': OpenApiResponse(description='Format is not available'), - # } - # ) + @extend_schema(summary='Export task as a dataset in a specific format', + parameters=[ + OpenApiParameter('format', location=OpenApiParameter.QUERY, + description='Desired output format name\nYou can get the list of supported formats at:\n/server/annotation/formats', + type=OpenApiTypes.STR, required=True), + OpenApiParameter('filename', description='Desired output file name', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=False), + OpenApiParameter('action', location=OpenApiParameter.QUERY, + description='Used to start downloading process after annotation file had been created', + type=OpenApiTypes.STR, required=False, enum=['download']) + ], + responses={ + '200': OpenApiResponse(description='Download of file started'), + '201': OpenApiResponse(description='Output file is ready for downloading'), + '202': OpenApiResponse(description='Exporting has been started'), + '405': OpenApiResponse(description='Format is not available'), + }, tags=['tasks'], versions=['v1']) @action(detail=True, methods=['GET'], serializer_class=None, url_path='dataset') def dataset_export(self, request, pk): @@ -1163,10 +958,26 @@ def dataset_export(self, request, pk): filename=request.query_params.get("filename", "").lower(), ) -# @method_decorator(name='retrieve', decorator=extend_schema(summary='Method returns details of a job')) -# @method_decorator(name='update', decorator=extend_schema(summary='Method updates a job by id')) -# @method_decorator(name='partial_update', decorator=extend_schema( -# summary='Methods does a partial update of chosen fields in a job')) +@extend_schema_view(retrieve=extend_schema( + summary='Method returns details of a job', + responses={ + '200': JobReadSerializer, + }, tags=['jobs'], versions=['v1'])) +@extend_schema_view(list=extend_schema( + summary='Method returns a paginated list of jobs according to query parameters', + responses={ + '200': JobReadSerializer(many=True), + }, tags=['jobs'], versions=['v1'])) +@extend_schema_view(update=extend_schema( + summary='Method updates a job by id', + responses={ + '200': JobWriteSerializer, + }, tags=['jobs'], versions=['v1'])) +@extend_schema_view(partial_update=extend_schema( + summary='Methods does a partial update of chosen fields in a job', + responses={ + '200': JobWriteSerializer, + }, tags=['jobs'], versions=['v1'])) class JobViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin): queryset = Job.objects.all().order_by('id') @@ -1185,15 +996,31 @@ def get_serializer_class(self): else: return JobWriteSerializer - # @extend_schema(methods=['GET'], summary='Method returns annotations for a specific job') - # @extend_schema(method='put', summary='Method performs an update of all annotations in a specific job') - # @extend_schema(methods=['PATCH'], parameters=[ - # OpenApiParameter('action', location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=True, - # enum=['create', 'update', 'delete'])], - # summary='Method performs a partial update of annotations in a specific job') - # @extend_schema(methods=['DELETE'], summary='Method deletes all annotations for a specific job') - # @action(detail=True, methods=['GET', 'DELETE', 'PUT', 'PATCH'], - # serializer_class=LabeledDataSerializer) + @extend_schema(methods=['GET'], summary='Method returns annotations for a specific job', + responses={ + '200': LabeledDataSerializer(many=True), + }, tags=['jobs'], versions=['v1']) + @extend_schema(methods=['PUT'], summary='Method performs an update of all annotations in a specific job', + request=AnnotationFileSerializer, responses={ + '201': OpenApiResponse(description='Uploading has finished'), + '202': OpenApiResponse(description='Uploading has been started'), + '405': OpenApiResponse(description='Format is not available'), + }, tags=['jobs'], versions=['v1']) + @extend_schema(methods=['PATCH'], summary='Method performs a partial update of annotations in a specific job', + parameters=[ + OpenApiParameter('action', location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, + required=True, enum=['create', 'update', 'delete']) + ], + responses={ + #TODO + '200': OpenApiResponse(), + }, tags=['jobs'], versions=['v1']) + @extend_schema(methods=['DELETE'], summary='Method deletes all annotations for a specific job', + responses={ + '204': OpenApiResponse('The annotation has been deleted'), + }, tags=['jobs'], versions=['v1']) + @action(detail=True, methods=['GET', 'DELETE', 'PUT', 'PATCH'], + serializer_class=LabeledDataSerializer) def annotations(self, request, pk): self.get_object() # force to call check_object_permissions if request.method == 'GET': @@ -1233,10 +1060,11 @@ def annotations(self, request, pk): return Response(data=str(e), status=status.HTTP_400_BAD_REQUEST) return Response(data) - # @extend_schema( - # method='get', - # summary='Method returns list of issues for the job', - # responses={'200': IssueReadSerializer(many=True)}) + @extend_schema( + summary='Method returns list of issues for the job', + responses={ + '200': IssueReadSerializer(many=True) + }, tags=['jobs'], versions=['v1']) @action(detail=True, methods=['GET'], serializer_class=IssueReadSerializer) def issues(self, request, pk): db_job = self.get_object() @@ -1246,19 +1074,21 @@ def issues(self, request, pk): return Response(serializer.data) - # @extend_schema(methods=['GET'], summary='Method returns data for a specific job', - # parameters=[ - # OpenApiParameter('type', location=OpenApiParameter.QUERY, required=True, type=OpenApiTypes.STR, - # enum=['chunk', 'frame', 'preview', 'context_image'], - # description="Specifies the type of the requested data"), - # OpenApiParameter('quality', location=OpenApiParameter.QUERY, required=True, type=OpenApiTypes.STR, - # enum=['compressed', 'original'], - # description="Specifies the quality level of the requested data, doesn't matter for 'preview' type"), - # OpenApiParameter('number', location=OpenApiParameter.QUERY, required=True, type=OpenApiTypes.NUMBER, - # description="A unique number value identifying chunk or frame, doesn't matter for 'preview' type"), - # ] - # ) - @action(detail=True) + @extend_schema(summary='Method returns data for a specific job', + parameters=[ + OpenApiParameter('type', description='Specifies the type of the requested data', + location=OpenApiParameter.QUERY, required=True, type=OpenApiTypes.STR, + enum=['chunk', 'frame', 'preview', 'context_image']), + OpenApiParameter('quality', location=OpenApiParameter.QUERY, required=True, + type=OpenApiTypes.STR, enum=['compressed', 'original'], + description="Specifies the quality level of the requested data, doesn't matter for 'preview' type"), + OpenApiParameter('number', location=OpenApiParameter.QUERY, required=True, type=OpenApiTypes.NUMBER, + description="A unique number value identifying chunk or frame, doesn't matter for 'preview' type"), + ], + responses={ + '200': OpenApiResponse(description='Data of a specific type'), + }, tags=['jobs'], versions=['v1']) + @action(detail=True, methods=['GET']) def data(self, request, pk): db_job = self.get_object() # call check_object_permissions as well data_type = request.query_params.get('type', None) @@ -1271,6 +1101,36 @@ def data(self, request, pk): return data_getter(request, db_job.segment.start_frame, db_job.segment.stop_frame, db_job.segment.task.data) +@extend_schema_view(retrieve=extend_schema( + summary='Method returns details of an issue', + responses={ + '200': IssueReadSerializer, + }, tags=['issues'], versions=['v1'])) +@extend_schema_view(list=extend_schema( + summary='Method returns a paginated list of issues according to query parameters', + responses={ + '200': IssueReadSerializer(many=True), + }, tags=['issues'], versions=['v1'])) +@extend_schema_view(update=extend_schema( + summary='Method updates an issue by id', + responses={ + '200': IssueWriteSerializer, + }, tags=['issues'], versions=['v1'])) +@extend_schema_view(partial_update=extend_schema( + summary='Methods does a partial update of chosen fields in an issue', + responses={ + '200': IssueWriteSerializer, + }, tags=['issues'], versions=['v1'])) +@extend_schema_view(create=extend_schema( + summary='Method creates an issue', + responses={ + '201': IssueWriteSerializer, + }, tags=['issues'], versions=['v1'])) +@extend_schema_view(destroy=extend_schema( + summary='Method deletes an issue', + responses={ + '204': OpenApiResponse('The issue has been deleted'), + }, tags=['issues'], versions=['v1'])) class IssueViewSet(viewsets.ModelViewSet): queryset = Issue.objects.all().order_by('-id') http_method_names = ['get', 'post', 'patch', 'delete', 'options'] @@ -1292,10 +1152,10 @@ def get_serializer_class(self): def perform_create(self, serializer): serializer.save(owner=self.request.user) - # @extend_schema( - # method='get', - # summary='The action returns all comments of a specific issue', - # responses={'200': CommentReadSerializer(many=True)}) + @extend_schema(summary='The action returns all comments of a specific issue', + responses={ + '200': CommentReadSerializer(many=True), + }, tags=['issues'], versions=['v1']) @action(detail=True, methods=['GET'], serializer_class=CommentReadSerializer) def comments(self, request, pk): db_issue = self.get_object() @@ -1305,6 +1165,36 @@ def comments(self, request, pk): return Response(serializer.data) +@extend_schema_view(retrieve=extend_schema( + summary='Method returns details of a comment', + responses={ + '200': CommentReadSerializer, + }, tags=['comments'], versions=['v1'])) +@extend_schema_view(list=extend_schema( + summary='Method returns a paginated list of comments according to query parameters', + responses={ + '200':CommentReadSerializer(many=True), + }, tags=['comments'], versions=['v1'])) +@extend_schema_view(update=extend_schema( + summary='Method updates a comment by id', + responses={ + '200': CommentWriteSerializer, + }, tags=['comments'], versions=['v1'])) +@extend_schema_view(partial_update=extend_schema( + summary='Methods does a partial update of chosen fields in a comment', + responses={ + '200': CommentWriteSerializer, + }, tags=['comments'], versions=['v1'])) +@extend_schema_view(create=extend_schema( + summary='Method creates a comment', + responses={ + '201': CommentWriteSerializer, + }, tags=['comments'], versions=['v1'])) +@extend_schema_view(destroy=extend_schema( + summary='Method deletes a comment', + responses={ + '204': OpenApiResponse('The comment has been deleted'), + }, tags=['comments'], versions=['v1'])) class CommentViewSet(viewsets.ModelViewSet): queryset = Comment.objects.all().order_by('-id') http_method_names = ['get', 'post', 'patch', 'delete', 'options'] @@ -1331,18 +1221,41 @@ class Meta: model = User fields = ("id", "is_active") -# @method_decorator(name='list', decorator=extend_schema( -# parameters=[ -# OpenApiParameter('id',OpenApiParameter.QUERY,description="A unique number value identifying this user",type=OpenApiTypes.NUMBER), -# OpenApiParameter('is_active',OpenApiParameter.QUERY,description="Returns only active users",type=OpenApiTypes.BOOL), -# ], -# summary='Method provides a paginated list of users registered on the server')) -# @method_decorator(name='retrieve', decorator=extend_schema( -# summary='Method provides information of a specific user')) -# @method_decorator(name='partial_update', decorator=extend_schema( -# summary='Method updates chosen fields of a user')) -# @method_decorator(name='destroy', decorator=extend_schema( -# summary='Method deletes a specific user from the server')) +@extend_schema_view(list=extend_schema( + summary='Method provides a paginated list of users registered on the server', + parameters=[ + OpenApiParameter('id', description='A unique number value identifying this user', + location=OpenApiParameter.QUERY, type=OpenApiTypes.NUMBER), + OpenApiParameter('is_active', description='Returns only active users', + location=OpenApiParameter.QUERY, type=OpenApiTypes.BOOL), + ], + responses={ + '200': PolymorphicProxySerializer(component_name='MetaUser', + serializers=[ + UserSerializer, BasicUserSerializer, + ], resource_type_field_name='type'), + }, tags=['users'], versions=['v1'])) +@extend_schema_view(retrieve=extend_schema( + summary='Method provides information of a specific user', + responses={ + '200': PolymorphicProxySerializer(component_name='MetaUser', + serializers=[ + UserSerializer, BasicUserSerializer, + ], resource_type_field_name='type'), + }, tags=['users'], versions=['v1'])) +@extend_schema_view(partial_update=extend_schema( + summary='Method updates chosen fields of a user', + responses={ + '200': PolymorphicProxySerializer(component_name='MetaUser', + serializers=[ + UserSerializer, BasicUserSerializer, + ], resource_type_field_name='type'), + }, tags=['users'], versions=['v1'])) +@extend_schema_view(destroy=extend_schema( + summary='Method deletes a specific user from the server', + responses={ + '204': OpenApiResponse('The user has been deleted'), + }, tags=['users'], versions=['v1'])) class UserViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin): queryset = User.objects.prefetch_related('groups').all().order_by('id') @@ -1370,7 +1283,13 @@ def get_serializer_class(self): else: return BasicUserSerializer - # @extend_schema(methods=['GET'], summary='Method returns an instance of a user who is currently authorized') + @extend_schema(summary='Method returns an instance of a user who is currently authorized', + responses={ + '200': PolymorphicProxySerializer(component_name='MetaUser', + serializers=[ + UserSerializer, BasicUserSerializer, + ], resource_type_field_name='type'), + }, tags=['users'], versions=['v1']) @action(detail=False, methods=['GET']) def self(self, request): """ @@ -1401,84 +1320,49 @@ class Meta: model = models.CloudStorage fields = ('id', 'display_name', 'provider_type', 'resource', 'credentials_type', 'description', 'owner') -@method_decorator( - name='retrieve', - decorator=extend_schema( - operation_id="CloudStorage - Retrieve", - summary='Method returns details of a specific cloud storage', - #request='application/json', - responses={ - '200': OpenApiResponse(description='A details of a storage'), - }, - tags=['cloud storages'], - versions=['v1'], - ) -) -@method_decorator(name='list', decorator=extend_schema( - operation_id='CloudStorage - List', - summary='Returns a paginated list of storages according to query parameters', - parameters=[ - OpenApiParameter( - 'provider_type', - location=OpenApiParameter.QUERY, - description='A supported provider of cloud storages', - type=OpenApiTypes.STR, - enum=CloudProviderChoice.list() - ), - OpenApiParameter( - 'display_name', - location=OpenApiParameter.QUERY, - description='A display name of storage', - type=OpenApiTypes.STR - ), - OpenApiParameter( - 'resource', - location=OpenApiParameter.QUERY, - description='A name of bucket or container', - type=OpenApiTypes.STR - ), - OpenApiParameter( - 'owner', - location=OpenApiParameter.QUERY, - description='A resource owner', - type=OpenApiTypes.STR - ), - OpenApiParameter( - 'credentials_type', - location=OpenApiParameter.QUERY, - description='A type of a granting access', - type=OpenApiTypes.STR, - enum=CredentialsTypeChoice.list() - ), - ], - responses={ - '200': BaseCloudStorageSerializer(many=True) - }, - tags=['cloud storages'], - versions=['v1'], - #FIXME - #field_inspectors=[RedefineDescriptionField] - ) -) -@method_decorator(name='destroy', decorator=extend_schema( - operation_id='CloudStorage - Delete', - summary='Method deletes a specific cloud storage', - responses={ - '204': OpenApiResponse(description='The cloud storage has been removed'), - }, - tags=['cloud storages'], - versions=['v1'], - ) -) -@method_decorator(name='partial_update', decorator=extend_schema( - operation_id='CloudStorage - PartialUpdate', - summary='Methods does a partial update of chosen fields in a cloud storage instance', - responses={}, - tags=['cloud storages'], - versions=['v1'], - #field_inspectors=[RedefineDescriptionField] - ) -) +@extend_schema_view(retrieve=extend_schema( + summary='Method returns details of a specific cloud storage', + responses={ + '200': ReadCloudStorageSerializer, + }, tags=['cloud storages'], versions=['v1'])) +@extend_schema_view(list=extend_schema( + summary='Returns a paginated list of storages according to query parameters', + parameters=[ + OpenApiParameter('provider_type', description='A supported provider of cloud storages', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, enum=CloudProviderChoice.list()), + OpenApiParameter('display_name', description='A display name of storage', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR), + OpenApiParameter('resource', description='A name of bucket or container', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR), + OpenApiParameter('owner', description='A resource owner', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR), + OpenApiParameter('credentials_type', description='A type of a granting access', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, enum=CredentialsTypeChoice.list()), + ], + #FIXME + #field_inspectors=[RedefineDescriptionField] + responses={ + '200': ReadCloudStorageSerializer(many=True), + }, tags=['cloud storages'], versions=['v1'])) +@extend_schema_view(destroy=extend_schema( + summary='Method deletes a specific cloud storage', + responses={ + '204': OpenApiResponse(description='The cloud storage has been removed'), + }, tags=['cloud storages'], versions=['v1'])) +@extend_schema_view(partial_update=extend_schema( + summary='Methods does a partial update of chosen fields in a cloud storage instance', + # FIXME + #field_inspectors=[RedefineDescriptionField] + responses={ + '200': WriteCloudStorageSerializer, + }, tags=['cloud storages'], versions=['v1'])) +@extend_schema_view(create=extend_schema( + summary='Method creates a cloud storage with a specified characteristics', + # FIXME + #field_inspectors=[RedefineDescriptionField], + responses={ + '201': WriteCloudStorageSerializer, + }, tags=['cloud storages'], versions=['v1'])) class CloudStorageViewSet(viewsets.ModelViewSet): http_method_names = ['get', 'post', 'patch', 'delete'] queryset = CloudStorageModel.objects.all().prefetch_related('data').order_by('-id') @@ -1487,9 +1371,9 @@ class CloudStorageViewSet(viewsets.ModelViewSet): def get_serializer_class(self): if self.request.method in ('POST', 'PATCH'): - return CloudStorageSerializer + return WriteCloudStorageSerializer else: - return BaseCloudStorageSerializer + return ReadCloudStorageSerializer def get_queryset(self): queryset = super().get_queryset() @@ -1514,17 +1398,6 @@ def perform_destroy(self, instance): super().perform_destroy(instance) shutil.rmtree(cloud_storage_dirname, ignore_errors=True) - @method_decorator(name='create', decorator=extend_schema( - operation_id='CloudStorage - Create', - summary='Method creates a cloud storage with a specified characteristics', - responses={ - '201': OpenApiResponse(description='A storage has beed created') - }, - tags=['cloud storages'], - versions=['v1'], - #field_inspectors=[RedefineDescriptionField], - ) - ) def create(self, request, *args, **kwargs): try: response = super().create(request, *args, **kwargs) @@ -1543,27 +1416,14 @@ def create(self, request, *args, **kwargs): response = HttpResponseBadRequest(str(ex)) return response - @extend_schema( - methods=['GET'], - summary='Method returns a manifest content', + @extend_schema(summary='Method returns a manifest content', parameters=[ - OpenApiParameter( - 'manifest_path', - location=OpenApiParameter.QUERY, - description='Path to the manifest file in a cloud storage', - type=OpenApiTypes.STR - ) + OpenApiParameter('manifest_path', description='Path to the manifest file in a cloud storage', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR), ], responses={ '200': OpenApiResponse(description='A manifest content'), - # TODO: describee all avaliable cases - '404': OpenApiResponse(description='The specified storage does not exist' - 'or real cloud storage was removed or ' - 'Specified manifest file does not exist on the cloud storage') - }, - tags=['cloud storages'], - versions=['v1'], - ) + }, tags=['cloud storages'], versions=['v1']) @action(detail=True, methods=['GET'], url_path='content') def content(self, request, pk): storage = None @@ -1620,17 +1480,10 @@ def content(self, request, pk): msg = str(ex) return HttpResponseBadRequest(msg) - @extend_schema( - methods=['GET'], - summary='Method returns a preview image from a cloud storage', + @extend_schema(summary='Method returns a preview image from a cloud storage', responses={ '200': OpenApiResponse(description='Cloud Storage preview'), - #TODO: - '404': OpenApiResponse(description=''), - }, - tags=['cloud storages'], - versions=['v1'], - ) + }, tags=['cloud storages'], versions=['v1']) @action(detail=True, methods=['GET'], url_path='preview') def preview(self, request, pk): storage = None @@ -1703,17 +1556,10 @@ def preview(self, request, pk): msg = str(ex) return HttpResponseBadRequest(msg) - @extend_schema( - methods=['GET'], - summary='Method returns a cloud storage status', + @extend_schema(summary='Method returns a cloud storage status', responses={ '200': OpenApiResponse(description='Cloud Storage status'), - #TODO - '404': OpenApiResponse(description=''), - }, - tags=['cloud storages'], - versions=['v1'], - ) + }, tags=['cloud storages'], versions=['v1']) @action(detail=True, methods=['GET'], url_path='status') def status(self, request, pk): try: From 1f55275ffcd0c95357c450dde10836b2dd11ac0c Mon Sep 17 00:00:00 2001 From: Maya Date: Thu, 20 Jan 2022 13:57:09 +0300 Subject: [PATCH 04/22] Update settings --- cvat/settings/base.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cvat/settings/base.py b/cvat/settings/base.py index d441afe6cbb..348d63cdc18 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -522,11 +522,25 @@ def add_ssh_keys(): 'DESCRIPTION': 'REST API for Computer Vision Annotation Tool (CVAT)', 'VERSION': 'v1', 'CONTACT': { + 'name': 'Nikita Manovich', + 'url': 'https://github.com/nmanovic', 'email': 'nikita.manovich@intel.com', }, 'LICENSE': { 'name': 'MIT License', + 'url': 'https://en.wikipedia.org/wiki/MIT_License', }, + 'SERVE_PUBLIC': True, + 'SCHEMA_COERCE_PATH_PK_SUFFIX': True, + 'SCHEMA_PATH_PREFIX': '/api/v[0-9]', + 'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'], + # 'SERVERS': [ + # { + # 'url': 'https://cvat.org', + # 'description': 'Production server', + # } + # ], + 'TERMS_OF_SERVICE': 'https://www.google.com/policies/terms/', # TODO, it's not an existing setting, so it doesn't display # OTHER SETTINGS # https://drf-spectacular.readthedocs.io/en/latest/settings.html } \ No newline at end of file From 99f79804b1e348a7695b0dd3f56e22607e001d38 Mon Sep 17 00:00:00 2001 From: Maya Date: Thu, 20 Jan 2022 13:59:06 +0300 Subject: [PATCH 05/22] temporary update other docs --- cvat/apps/iam/views.py | 36 ++++++++++++++----------------- cvat/apps/lambda_manager/views.py | 10 +++++++-- cvat/apps/restrictions/views.py | 11 ++++++---- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/cvat/apps/iam/views.py b/cvat/apps/iam/views.py index 88d3574756e..8864d4df368 100644 --- a/cvat/apps/iam/views.py +++ b/cvat/apps/iam/views.py @@ -4,7 +4,7 @@ from django.core.exceptions import BadRequest from django.utils.functional import SimpleLazyObject -from rest_framework import views +from rest_framework import views, serializers from rest_framework.exceptions import ValidationError from django.conf import settings from rest_framework.response import Response @@ -12,8 +12,7 @@ from allauth.account import app_settings as allauth_settings from furl import furl -from django.utils.decorators import method_decorator - +from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer, extend_schema_view from .authentication import Signer @@ -73,25 +72,22 @@ def __call__(self, request): return self.get_response(request) -# @method_decorator(name='post', decorator=swagger_auto_schema( -# request_body=openapi.Schema( -# type=openapi.TYPE_OBJECT, -# required=[ -# 'url' -# ], -# properties={ -# 'url': openapi.Schema(type=openapi.TYPE_STRING) -# } -# ), -# responses={'200': openapi.Response(description='text URL')} -# )) +@extend_schema_view(post=extend_schema( + summary='This method signs URL for access to the server', + description='Signed URL contains a token which authenticates a user on the server.' + 'Signed URL is valid during 30 seconds since signing.', + request=inline_serializer( + name='Signing', + fields={ + 'url': serializers.CharField(), + } + ), + responses={'200': OpenApiResponse(description='text URL')}, + tags=['auth'], + versions=['v1'], +)) class SigningView(views.APIView): - """ - This method signs URL for access to the server. - Signed URL contains a token which authenticates a user on the server. - Signed URL is valid during 30 seconds since signing. - """ def post(self, request): url = request.data.get('url') if not url: diff --git a/cvat/apps/lambda_manager/views.py b/cvat/apps/lambda_manager/views.py index a0f40842f5f..3365922d6d6 100644 --- a/cvat/apps/lambda_manager/views.py +++ b/cvat/apps/lambda_manager/views.py @@ -17,6 +17,9 @@ from cvat.apps.engine.serializers import LabeledDataSerializer from cvat.apps.engine.models import ShapeType, SourceType +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema, extend_schema_view + class LambdaType(Enum): DETECTOR = "detector" INTERACTOR = "interactor" @@ -228,7 +231,6 @@ def _get_image(self, db_task, frame, quality): return base64.b64encode(image[0].getvalue()).decode('utf-8') - class LambdaQueue: def _get_queue(self): QUEUE_NAME = "low" @@ -286,7 +288,6 @@ def fetch_job(self, pk): return LambdaJob(job) - class LambdaJob: def __init__(self, job): self.job = job @@ -552,6 +553,8 @@ def func_wrapper(*args, **kwargs): return func_wrapper return wrap_response +@extend_schema_view(retrieve=extend_schema(summary='', tags=['lambda'], versions=['v1'])) +@extend_schema_view(list=extend_schema(summary='', tags=['lambda'], versions=['v1'])) class FunctionViewSet(viewsets.ViewSet): lookup_value_regex = '[a-zA-Z0-9_.-]+' lookup_field = 'func_id' @@ -584,6 +587,9 @@ def call(self, request, func_id): return lambda_func.invoke(db_task, request.data) +@extend_schema_view(retrieve=extend_schema(summary='', tags=['lambda'], versions=['v1'])) +@extend_schema_view(list=extend_schema(summary='', tags=['lambda'], versions=['v1'])) +@extend_schema_view(delete=extend_schema(summary='', tags=['lambda'], versions=['v1'])) class RequestViewSet(viewsets.ViewSet): @return_response() def list(self, request): diff --git a/cvat/apps/restrictions/views.py b/cvat/apps/restrictions/views.py index c446f3e593d..0ef15ef8058 100644 --- a/cvat/apps/restrictions/views.py +++ b/cvat/apps/restrictions/views.py @@ -8,6 +8,7 @@ from rest_framework import viewsets from rest_framework.permissions import AllowAny from rest_framework.renderers import TemplateHTMLRenderer +from drf_spectacular.utils import OpenApiResponse, extend_schema from cvat.apps.restrictions.serializers import UserAgreementSerializer @@ -23,10 +24,9 @@ def get_serializer(self, *args, **kwargs): pass @staticmethod - # @swagger_auto_schema( - # method='get', - # operation_summary='Method provides user agreements that the user must accept to register', - # responses={'200': UserAgreementSerializer}) + @extend_schema(summary='Method provides user agreements that the user must accept to register', + responses={'200': UserAgreementSerializer}, + tags=['restrictions'], versions=['v1']) @action(detail=False, methods=['GET'], serializer_class=UserAgreementSerializer, url_path='user-agreements') def user_agreements(request): user_agreements = settings.RESTRICTIONS['user_agreements'] @@ -35,6 +35,9 @@ def user_agreements(request): return Response(data=serializer.data) @staticmethod + @extend_schema(summary='Method provides CVAT terms of use', + responses={'200': OpenApiResponse(description='CVAT terms of use')}, + tags=['restrictions'], versions=['v1']) @action(detail=False, methods=['GET'], renderer_classes=(TemplateHTMLRenderer,), url_path='terms-of-use') def terms_of_use(request): From c2b2742962dbb4521657caede36dcc2e330262f5 Mon Sep 17 00:00:00 2001 From: Maya Date: Mon, 24 Jan 2022 12:11:27 +0300 Subject: [PATCH 06/22] Update serializer name --- cvat/apps/engine/serializers.py | 4 ++-- cvat/apps/engine/views.py | 23 ++++++++++++----------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 3e008e29c99..e426830e787 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -863,7 +863,7 @@ def to_internal_value(self, data): def to_representation(self, instance): return instance.filename if instance else instance -class ReadCloudStorageSerializer(serializers.ModelSerializer): +class CloudStorageReadSerializer(serializers.ModelSerializer): owner = BasicUserSerializer(required=False) manifests = ManifestSerializer(many=True, default=[]) class Meta: @@ -934,7 +934,7 @@ class Meta: ) ] ) -class WriteCloudStorageSerializer(serializers.ModelSerializer): +class CloudStorageWriteSerializer(serializers.ModelSerializer): owner = BasicUserSerializer(required=False) session_token = serializers.CharField(max_length=440, allow_blank=True, required=False) key = serializers.CharField(max_length=20, allow_blank=True, required=False) diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 9048abaa671..d4d662da42c 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -6,7 +6,6 @@ import io import os import os.path as osp -from pydoc import resolve import pytz import shutil import traceback @@ -28,10 +27,10 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import ( - OpenApiExample, OpenApiParameter, OpenApiResponse, extend_schema, PolymorphicProxySerializer, extend_schema_view + OpenApiExample, OpenApiParameter, OpenApiResponse, PolymorphicProxySerializer, + extend_schema_view, extend_schema ) - from rest_framework import mixins, serializers, status, viewsets from rest_framework.decorators import action from rest_framework.exceptions import APIException, NotFound, ValidationError @@ -39,6 +38,7 @@ from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from rest_framework.exceptions import PermissionDenied +#from rest_framework.versioning import NamespaceVersioning from sendfile import sendfile import cvat.apps.dataset_manager as dm @@ -61,8 +61,8 @@ FileInfoSerializer, JobReadSerializer, JobWriteSerializer, LabeledDataSerializer, LogEventSerializer, ProjectSerializer, ProjectSearchSerializer, RqStatusSerializer, TaskSerializer, UserSerializer, PluginsSerializer, IssueReadSerializer, - IssueWriteSerializer, CommentReadSerializer, CommentWriteSerializer, WriteCloudStorageSerializer, - ReadCloudStorageSerializer, DatasetFileSerializer) + IssueWriteSerializer, CommentReadSerializer, CommentWriteSerializer, CloudStorageWriteSerializer, + CloudStorageReadSerializer, DatasetFileSerializer) from utils.dataset_manifest import ImageManifestManager from cvat.apps.engine.utils import av_scan_paths @@ -1323,7 +1323,7 @@ class Meta: @extend_schema_view(retrieve=extend_schema( summary='Method returns details of a specific cloud storage', responses={ - '200': ReadCloudStorageSerializer, + '200': CloudStorageReadSerializer, }, tags=['cloud storages'], versions=['v1'])) @extend_schema_view(list=extend_schema( summary='Returns a paginated list of storages according to query parameters', @@ -1342,7 +1342,7 @@ class Meta: #FIXME #field_inspectors=[RedefineDescriptionField] responses={ - '200': ReadCloudStorageSerializer(many=True), + '200': CloudStorageReadSerializer(many=True), }, tags=['cloud storages'], versions=['v1'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes a specific cloud storage', @@ -1354,26 +1354,27 @@ class Meta: # FIXME #field_inspectors=[RedefineDescriptionField] responses={ - '200': WriteCloudStorageSerializer, + '200': CloudStorageWriteSerializer, }, tags=['cloud storages'], versions=['v1'])) @extend_schema_view(create=extend_schema( summary='Method creates a cloud storage with a specified characteristics', # FIXME #field_inspectors=[RedefineDescriptionField], responses={ - '201': WriteCloudStorageSerializer, + '201': CloudStorageWriteSerializer, }, tags=['cloud storages'], versions=['v1'])) class CloudStorageViewSet(viewsets.ModelViewSet): http_method_names = ['get', 'post', 'patch', 'delete'] queryset = CloudStorageModel.objects.all().prefetch_related('data').order_by('-id') search_fields = ('provider_type', 'display_name', 'resource', 'credentials_type', 'owner__username', 'description') filterset_class = CloudStorageFilter + #versioning_class = NamespaceVersioning def get_serializer_class(self): if self.request.method in ('POST', 'PATCH'): - return WriteCloudStorageSerializer + return CloudStorageWriteSerializer else: - return ReadCloudStorageSerializer + return CloudStorageReadSerializer def get_queryset(self): queryset = super().get_queryset() From 65854b311cb9e0749a4e20e23b4679053d94f87b Mon Sep 17 00:00:00 2001 From: Maya Date: Mon, 24 Jan 2022 12:11:45 +0300 Subject: [PATCH 07/22] Update SPECTACULAR_SETTINGS --- cvat/settings/base.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 348d63cdc18..9e2aa511c6e 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -532,15 +532,32 @@ def add_ssh_keys(): }, 'SERVE_PUBLIC': True, 'SCHEMA_COERCE_PATH_PK_SUFFIX': True, - 'SCHEMA_PATH_PREFIX': '/api/v[0-9]', + 'SCHEMA_PATH_PREFIX': r'/api/v[0-9]', + 'SCHEMA_PATH_PREFIX_TRIM': True, 'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'], - # 'SERVERS': [ - # { - # 'url': 'https://cvat.org', - # 'description': 'Production server', - # } - # ], - 'TERMS_OF_SERVICE': 'https://www.google.com/policies/terms/', # TODO, it's not an existing setting, so it doesn't display + 'SERVERS': [ + { + 'url': 'https://localhost:7000/', + 'description': 'Development server', + 'variables': { + 'protocol': { + 'enum': ['http', 'https'], + 'default': 'http', + } + }, + } + ], + 'SWAGGER_UI_SETTINGS': { + 'deepLinking': True, + 'displayOperationId': True, + 'displayRequestDuration': True, + 'filter': True, + }, + 'TOS': 'https://www.google.com/policies/terms/', + 'EXTERNAL_DOCS': { + 'description': 'CVAT documentation', + 'url': 'https://openvinotoolkit.github.io/cvat/docs/', + }, # OTHER SETTINGS # https://drf-spectacular.readthedocs.io/en/latest/settings.html } \ No newline at end of file From 7b5f7895742306d553f80c7fb021c3f85ab4708f Mon Sep 17 00:00:00 2001 From: Maya Date: Mon, 24 Jan 2022 12:12:27 +0300 Subject: [PATCH 08/22] Add documentation --- cvat/apps/iam/views.py | 5 +- cvat/apps/lambda_manager/views.py | 19 ++++--- cvat/apps/organizations/views.py | 90 ++++++++++++++++++++++++++++++- 3 files changed, 103 insertions(+), 11 deletions(-) diff --git a/cvat/apps/iam/views.py b/cvat/apps/iam/views.py index 8864d4df368..da57c45f5af 100644 --- a/cvat/apps/iam/views.py +++ b/cvat/apps/iam/views.py @@ -82,10 +82,7 @@ def __call__(self, request): 'url': serializers.CharField(), } ), - responses={'200': OpenApiResponse(description='text URL')}, - tags=['auth'], - versions=['v1'], -)) + responses={'200': OpenApiResponse(description='text URL')}, tags=['auth'], versions=['v1'])) class SigningView(views.APIView): def post(self, request): diff --git a/cvat/apps/lambda_manager/views.py b/cvat/apps/lambda_manager/views.py index 3365922d6d6..8fd2d4e1e91 100644 --- a/cvat/apps/lambda_manager/views.py +++ b/cvat/apps/lambda_manager/views.py @@ -17,7 +17,6 @@ from cvat.apps.engine.serializers import LabeledDataSerializer from cvat.apps.engine.models import ShapeType, SourceType -from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema, extend_schema_view class LambdaType(Enum): @@ -553,8 +552,10 @@ def func_wrapper(*args, **kwargs): return func_wrapper return wrap_response -@extend_schema_view(retrieve=extend_schema(summary='', tags=['lambda'], versions=['v1'])) -@extend_schema_view(list=extend_schema(summary='', tags=['lambda'], versions=['v1'])) +@extend_schema_view(retrieve=extend_schema( + summary='Method returns the information about the function', tags=['lambda'], versions=['v1'])) +@extend_schema_view(list=extend_schema( + summary='Method returns a list of functions', tags=['lambda'], versions=['v1'])) class FunctionViewSet(viewsets.ViewSet): lookup_value_regex = '[a-zA-Z0-9_.-]+' lookup_field = 'func_id' @@ -587,9 +588,15 @@ def call(self, request, func_id): return lambda_func.invoke(db_task, request.data) -@extend_schema_view(retrieve=extend_schema(summary='', tags=['lambda'], versions=['v1'])) -@extend_schema_view(list=extend_schema(summary='', tags=['lambda'], versions=['v1'])) -@extend_schema_view(delete=extend_schema(summary='', tags=['lambda'], versions=['v1'])) +@extend_schema_view(retrieve=extend_schema( + summary='Method returns the status of the request', tags=['lambda'], versions=['v1'])) +@extend_schema_view(list=extend_schema( + summary='Method returns a list of requests', tags=['lambda'], versions=['v1'])) +#TODO +@extend_schema_view(create=extend_schema( + summary='Method calls the function', tags=['lambda'], versions=['v1'])) +@extend_schema_view(delete=extend_schema( + summary='Method cancels the request', tags=['lambda'], versions=['v1'])) class RequestViewSet(viewsets.ViewSet): @return_response() def list(self, request): diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index 1ba702f422e..f23d5f67076 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -7,6 +7,8 @@ from django.utils.crypto import get_random_string from django_filters import rest_framework as filters +from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view + from cvat.apps.iam.permissions import ( InvitationPermission, MembershipPermission, OrganizationPermission) from .models import Invitation, Membership, Organization @@ -16,6 +18,37 @@ MembershipReadSerializer, MembershipWriteSerializer, OrganizationReadSerializer, OrganizationWriteSerializer) + +@extend_schema_view(retrieve=extend_schema( + summary='Method returns details of an organization', + responses={ + '200': OrganizationReadSerializer, + }, tags=['organizations'])) # versions=['v1'] +@extend_schema_view(list=extend_schema( + summary='Method returns a paginated list of organizatins according to query parameters', + responses={ + '200': OrganizationReadSerializer(many=True), + }, tags=['organizations']))# versions=['v1'] +@extend_schema_view(update=extend_schema( + summary='Method updates an organization by id', + responses={ + '200': OrganizationWriteSerializer, + }, tags=['organizations'])) # versions=['v1'] +@extend_schema_view(partial_update=extend_schema( + summary='Methods does a partial update of chosen fields in an organization', + responses={ + '200': OrganizationWriteSerializer, + }, tags=['organizations'])) # versions=['v1'] +@extend_schema_view(create=extend_schema( + summary='Method creates an organization', + responses={ + '201': OrganizationWriteSerializer, + }, tags=['organizations'])) # versions=['v1'] +@extend_schema_view(destroy=extend_schema( + summary='Method deletes an organization', + responses={ + '204': OpenApiResponse('The organization has been deleted'), + }, tags=['organizations'])) # versions=['v1'] class OrganizationViewSet(viewsets.ModelViewSet): queryset = Organization.objects.all() ordering = ['-id'] @@ -45,7 +78,31 @@ class MembershipFilter(filters.FilterSet): class Meta: model = Membership fields = ("user", ) - +@extend_schema_view(retrieve=extend_schema( + summary='Method returns details of a membership', + responses={ + '200': MembershipReadSerializer, + }, tags=['memberships'], versions=['v1'])) +@extend_schema_view(list=extend_schema( + summary='Method returns a paginated list of memberships according to query parameters', + responses={ + '200': MembershipReadSerializer(many=True), + }, tags=['memberships'], versions=['v1'])) +@extend_schema_view(update=extend_schema( + summary='Method updates a membership by id', + responses={ + '200': MembershipWriteSerializer, + }, tags=['memberships'], versions=['v1'])) +@extend_schema_view(partial_update=extend_schema( + summary='Methods does a partial update of chosen fields in a membership', + responses={ + '200': MembershipWriteSerializer, + }, tags=['memberships'], versions=['v1'])) +@extend_schema_view(destroy=extend_schema( + summary='Method deletes a membership', + responses={ + '204': OpenApiResponse('The membership has been deleted'), + }, tags=['memberships'], versions=['v1'])) class MembershipViewSet(mixins.RetrieveModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet): queryset = Membership.objects.all() @@ -64,6 +121,37 @@ def get_queryset(self): permission = MembershipPermission(self.request, self) return permission.filter(queryset) +# TODO +@extend_schema_view(retrieve=extend_schema( + summary='Method returns details of an invitation', + responses={ + '200': InvitationReadSerializer, + }, tags=['invitations'], versions=['v1'])) +@extend_schema_view(list=extend_schema( + summary='Method returns a paginated list of invitations according to query parameters', + responses={ + '200': InvitationReadSerializer(many=True), + }, tags=['invitations'], versions=['v1'])) +@extend_schema_view(update=extend_schema( + summary='Method updates an invitation by id', + responses={ + '200': InvitationWriteSerializer, + }, tags=['invitations'], versions=['v1'])) +@extend_schema_view(partial_update=extend_schema( + summary='Methods does a partial update of chosen fields in an invitation', + responses={ + '200': InvitationWriteSerializer, + }, tags=['invitations'], versions=['v1'])) +@extend_schema_view(create=extend_schema( + summary='Method creates an invitation', + responses={ + '201': InvitationWriteSerializer, + }, tags=['invitations'], versions=['v1'])) +@extend_schema_view(destroy=extend_schema( + summary='Method deletes an invitation', + responses={ + '204': OpenApiResponse('The invitation has been deleted'), + }, tags=['invitations'], versions=['v1'])) class InvitationViewSet(viewsets.ModelViewSet): queryset = Invitation.objects.all() ordering = ['-created_date'] From 833e58fa48cff6c93a53af8688d9d9f60a170141 Mon Sep 17 00:00:00 2001 From: Maya Date: Mon, 24 Jan 2022 12:16:51 +0300 Subject: [PATCH 09/22] temporary update of urls --- cvat/apps/engine/urls.py | 57 ++++++-------------------------- cvat/apps/iam/urls.py | 1 + cvat/apps/lambda_manager/urls.py | 5 ++- cvat/apps/organizations/urls.py | 4 ++- cvat/apps/training/urls.py | 3 ++ 5 files changed, 22 insertions(+), 48 deletions(-) diff --git a/cvat/apps/engine/urls.py b/cvat/apps/engine/urls.py index 225dd9534df..81192835f25 100644 --- a/cvat/apps/engine/urls.py +++ b/cvat/apps/engine/urls.py @@ -6,7 +6,7 @@ from django.urls import path, include from . import views from rest_framework import routers -from rest_framework import permissions +#from rest_framework.versioning import NamespaceVersioning from django.views.generic import RedirectView from django.conf import settings @@ -16,46 +16,18 @@ from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView -# schema_view = get_schema_view( -# openapi.Info( -# title="CVAT REST API", -# default_version='v1', -# description="REST API for Computer Vision Annotation Tool (CVAT)", -# terms_of_service="https://www.google.com/policies/terms/", -# contact=openapi.Contact(email="nikita.manovich@intel.com"), -# license=openapi.License(name="MIT License"), -# ), -# public=True, -# permission_classes=(permissions.IsAuthenticated,), -# ) - -# drf-yasg component doesn't handle correctly URL_FORMAT_OVERRIDE and -# send requests with ?format=openapi suffix instead of ?scheme=openapi. -# We map the required parameter explicitly and add it into query arguments -# on the server side. -# def wrap_swagger(view): -# @login_required -# def _map_format_to_schema(request, scheme=None): -# if 'format' in request.GET: -# request.GET = request.GET.copy() -# format_alias = settings.REST_FRAMEWORK['URL_FORMAT_OVERRIDE'] -# request.GET[format_alias] = request.GET['format'] - -# return view(request, format=scheme) - -# return _map_format_to_schema router = routers.DefaultRouter(trailing_slash=False) -router.register('projects', views.ProjectViewSet, basename='projects') -router.register('tasks', views.TaskViewSet, basename='tasks') -router.register('jobs', views.JobViewSet, basename='jobs') -router.register('users', views.UserViewSet, basename='users') +router.register('projects', views.ProjectViewSet) +router.register('tasks', views.TaskViewSet) +router.register('jobs', views.JobViewSet) +router.register('users', views.UserViewSet) router.register('server', views.ServerViewSet, basename='server') -router.register('issues', views.IssueViewSet, basename='issues') -router.register('comments', views.CommentViewSet, basename='comments') +router.register('issues', views.IssueViewSet) +router.register('comments', views.CommentViewSet) router.register('restrictions', RestrictionsViewSet, basename='restrictions') router.register('predict', PredictView, basename='predict') -router.register('cloudstorages', views.CloudStorageViewSet, basename='cloudstorages') +router.register('cloudstorages', views.CloudStorageViewSet) urlpatterns = [ # Entry point for a client @@ -63,19 +35,12 @@ query_string=True)), # documentation for API - # path('api/swagger', wrap_swagger( - # schema_view.without_ui(cache_timeout=0)), name='schema-json'), - # path('api/swagger/', wrap_swagger( - # schema_view.with_ui('swagger', cache_timeout=0)), name='schema-swagger-ui'), - # path('api/docs/', wrap_swagger( - # schema_view.with_ui('redoc', cache_timeout=0)), name='schema-redoc'), - - path('api/schema/', SpectacularAPIView.as_view(api_version='v1'), name='schema'), + path('api/schema/', SpectacularAPIView.as_view(api_version='v1'), name='schema'), # versioning_class=NamespaceVersioning path('api/swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger'), - path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), + path('api/docs/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), # entry point for API path('api/v1/', include('cvat.apps.iam.urls')), path('api/v1/', include('cvat.apps.organizations.urls')), - path('api/v1/', include((router.urls, 'cvat'), namespace='v1')), + path('api/v1/', include(router.urls)), ] diff --git a/cvat/apps/iam/urls.py b/cvat/apps/iam/urls.py index cfc3c441a4f..049ff8ed949 100644 --- a/cvat/apps/iam/urls.py +++ b/cvat/apps/iam/urls.py @@ -38,4 +38,5 @@ name='account_email_verification_sent'), ] +#urlpatterns = [path('auth/', include(urlpatterns))] urlpatterns = [path('auth/', include((urlpatterns, 'cvat'), namespace='v1'))] \ No newline at end of file diff --git a/cvat/apps/lambda_manager/urls.py b/cvat/apps/lambda_manager/urls.py index f490f714db0..7e75bca9eae 100644 --- a/cvat/apps/lambda_manager/urls.py +++ b/cvat/apps/lambda_manager/urls.py @@ -24,6 +24,9 @@ # GET /api/v1/lambda/requests - get list of requests # GET /api/v1/lambda/requests/ - get status of the request # DEL /api/v1/lambda/requests/ - cancel a request (don't delete) +# urlpatterns = [ +# path('api/v1/lambda/', include((router.urls, 'lambda_manager'), namespace='v1')) +# ] urlpatterns = [ path('api/v1/lambda/', include((router.urls, 'cvat'), namespace='v1')) -] +] \ No newline at end of file diff --git a/cvat/apps/organizations/urls.py b/cvat/apps/organizations/urls.py index d0d0b23d222..164e9b5c069 100644 --- a/cvat/apps/organizations/urls.py +++ b/cvat/apps/organizations/urls.py @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: MIT +#from django.urls import include, path from rest_framework.routers import DefaultRouter from .views import InvitationViewSet, MembershipViewSet, OrganizationViewSet @@ -10,4 +11,5 @@ router.register('invitations', InvitationViewSet) router.register('memberships', MembershipViewSet) -urlpatterns = router.urls \ No newline at end of file +urlpatterns = router.urls +#urlpatterns = [path('', include((router.urls, 'organizations'), namespace='v1'))] diff --git a/cvat/apps/training/urls.py b/cvat/apps/training/urls.py index 47ce86bfb0e..3aa31d9e73e 100644 --- a/cvat/apps/training/urls.py +++ b/cvat/apps/training/urls.py @@ -6,6 +6,9 @@ router = routers.DefaultRouter(trailing_slash=False) router.register('', PredictView, basename='predict') +# urlpatterns = [ +# path('', include((router.urls, 'predict'), namespace='v1')) +# ] urlpatterns = [ path('', include((router.urls, 'predict'), namespace='predict')) ] From 83824c2e29a09baf8a61d2417d0c2c8b4ee100e3 Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 25 Jan 2022 13:41:50 +0300 Subject: [PATCH 10/22] Revert urls --- cvat/apps/engine/urls.py | 5 +---- cvat/apps/iam/urls.py | 3 +-- cvat/apps/organizations/urls.py | 1 - cvat/apps/training/urls.py | 3 --- 4 files changed, 2 insertions(+), 10 deletions(-) diff --git a/cvat/apps/engine/urls.py b/cvat/apps/engine/urls.py index 81192835f25..140a9bd3e07 100644 --- a/cvat/apps/engine/urls.py +++ b/cvat/apps/engine/urls.py @@ -6,17 +6,14 @@ from django.urls import path, include from . import views from rest_framework import routers -#from rest_framework.versioning import NamespaceVersioning from django.views.generic import RedirectView from django.conf import settings from cvat.apps.restrictions.views import RestrictionsViewSet -from cvat.apps.iam.decorators import login_required from cvat.apps.training.views import PredictView from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView - router = routers.DefaultRouter(trailing_slash=False) router.register('projects', views.ProjectViewSet) router.register('tasks', views.TaskViewSet) @@ -35,7 +32,7 @@ query_string=True)), # documentation for API - path('api/schema/', SpectacularAPIView.as_view(api_version='v1'), name='schema'), # versioning_class=NamespaceVersioning + path('api/schema/', SpectacularAPIView.as_view(api_version='1.0'), name='schema'), path('api/swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger'), path('api/docs/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), diff --git a/cvat/apps/iam/urls.py b/cvat/apps/iam/urls.py index 049ff8ed949..76b0d3192fc 100644 --- a/cvat/apps/iam/urls.py +++ b/cvat/apps/iam/urls.py @@ -38,5 +38,4 @@ name='account_email_verification_sent'), ] -#urlpatterns = [path('auth/', include(urlpatterns))] -urlpatterns = [path('auth/', include((urlpatterns, 'cvat'), namespace='v1'))] \ No newline at end of file +urlpatterns = [path('auth/', include(urlpatterns))] \ No newline at end of file diff --git a/cvat/apps/organizations/urls.py b/cvat/apps/organizations/urls.py index 164e9b5c069..79438727e24 100644 --- a/cvat/apps/organizations/urls.py +++ b/cvat/apps/organizations/urls.py @@ -12,4 +12,3 @@ router.register('memberships', MembershipViewSet) urlpatterns = router.urls -#urlpatterns = [path('', include((router.urls, 'organizations'), namespace='v1'))] diff --git a/cvat/apps/training/urls.py b/cvat/apps/training/urls.py index 3aa31d9e73e..47ce86bfb0e 100644 --- a/cvat/apps/training/urls.py +++ b/cvat/apps/training/urls.py @@ -6,9 +6,6 @@ router = routers.DefaultRouter(trailing_slash=False) router.register('', PredictView, basename='predict') -# urlpatterns = [ -# path('', include((router.urls, 'predict'), namespace='v1')) -# ] urlpatterns = [ path('', include((router.urls, 'predict'), namespace='predict')) ] From dd33c46555d0fff09eb6b08c8b880335e66219df Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 25 Jan 2022 13:47:47 +0300 Subject: [PATCH 11/22] Update versions --- cvat/apps/engine/views.py | 142 +++++++++++++++--------------- cvat/apps/iam/views.py | 2 +- cvat/apps/lambda_manager/views.py | 12 +-- cvat/apps/organizations/views.py | 34 +++---- cvat/apps/restrictions/views.py | 4 +- cvat/settings/base.py | 4 +- 6 files changed, 99 insertions(+), 99 deletions(-) diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index d4d662da42c..6da1d2cea85 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -88,7 +88,7 @@ def get_serializer(self, *args, **kwargs): responses={ '200': AboutSerializer, }, - tags=['server'], versions=['v1']) + tags=['server'], versions=['1.0']) @action(detail=False, methods=['GET'], serializer_class=AboutSerializer) def about(request): from cvat import __version__ as cvat_version @@ -113,7 +113,7 @@ def about(request): request=ExceptionSerializer, responses={ '201': ExceptionSerializer, }, - tags=['server'], versions=['v1']) + tags=['server'], versions=['1.0']) @action(detail=False, methods=['POST'], serializer_class=ExceptionSerializer) def exception(request): serializer = ExceptionSerializer(data=request.data) @@ -140,7 +140,7 @@ def exception(request): request=LogEventSerializer(many=True), responses={ '201': LogEventSerializer(many=True), - }, tags=['server'], versions=['v1']) + }, tags=['server'], versions=['1.0']) @action(detail=False, methods=['POST'], serializer_class=LogEventSerializer) def logs(request): serializer = LogEventSerializer(many=True, data=request.data) @@ -167,7 +167,7 @@ def logs(request): ], responses={ '200' : FileInfoSerializer(many=True) - }, tags=['server'], versions=['v1']) + }, tags=['server'], versions=['1.0']) @action(detail=False, methods=['GET'], serializer_class=FileInfoSerializer) def share(request): param = request.query_params.get('directory', '/') @@ -200,7 +200,7 @@ def share(request): summary='Method provides the list of supported annotations formats', responses={ '200': DatasetFormatsSerializer, - }, tags=['server'], versions=['v1']) + }, tags=['server'], versions=['1.0']) @action(detail=False, methods=['GET'], url_path='annotation/formats') def annotation_formats(request): data = dm.views.get_all_formats() @@ -211,7 +211,7 @@ def annotation_formats(request): summary='Method provides allowed plugins', responses={ '200': PluginsSerializer, - }, tags=['server'], versions=['v1']) + }, tags=['server'], versions=['1.0']) @action(detail=False, methods=['GET'], url_path='plugins', serializer_class=PluginsSerializer) def plugins(request): response = { @@ -256,27 +256,27 @@ class Meta: serializers=[ ProjectSerializer, ProjectSearchSerializer, ], resource_type_field_name='type', many=True), - }, tags=['projects'], versions=['v1'])) + }, tags=['projects'], versions=['1.0'])) @extend_schema_view(create=extend_schema( summary='Method creates a new project', responses={ '201': ProjectSerializer, - }, tags=['projects'], versions=['v1'])) + }, tags=['projects'], versions=['1.0'])) @extend_schema_view(retrieve=extend_schema( summary='Method returns details of a specific project', responses={ '200': ProjectSerializer, - }, tags=['projects'], versions=['v1'])) + }, tags=['projects'], versions=['1.0'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes a specific project', responses={ '204': OpenApiResponse(description='The project has been deleted'), - }, tags=['projects'], versions=['v1'])) + }, tags=['projects'], versions=['1.0'])) @extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in a project', responses={ '200': ProjectSerializer, - }, tags=['projects'], versions=['v1'])) + }, tags=['projects'], versions=['1.0'])) class ProjectViewSet(viewsets.ModelViewSet): queryset = models.Project.objects.prefetch_related(Prefetch('label_set', queryset=models.Label.objects.order_by('id') @@ -311,7 +311,7 @@ def perform_create(self, serializer): summary='Method returns information of the tasks of the project with the selected id', responses={ '200': TaskSerializer(many=True), - }, tags=['projects'], versions=['v1']) + }, tags=['projects'], versions=['1.0']) @action(detail=True, methods=['GET'], serializer_class=TaskSerializer) def tasks(self, request, pk): self.get_object() # force to call check_object_permissions @@ -343,7 +343,7 @@ def tasks(self, request, pk): '201': OpenApiResponse(description='Output file is ready for downloading'), '202': OpenApiResponse(description='Exporting has been started'), '405': OpenApiResponse(description='Format is not available'), - }, tags=['projects'], versions=['v1']) + }, tags=['projects'], versions=['1.0']) @extend_schema(methods=['POST'], summary='Import dataset in specific format as a project', parameters=[ OpenApiParameter('format', description='Desired dataset format name\n' @@ -354,7 +354,7 @@ def tasks(self, request, pk): '202': OpenApiResponse(description='Exporting has been started'), '400': OpenApiResponse(description='Failed to import dataset'), '405': OpenApiResponse(description='Format is not available'), - }, tags=['projects'], versions=['v1']) + }, tags=['projects'], versions=['1.0']) @action(detail=True, methods=['GET', 'POST'], serializer_class=None, url_path='dataset') def dataset(self, request, pk): @@ -423,7 +423,7 @@ def dataset(self, request, pk): '202': OpenApiResponse(description='Dump of annotations has been started'), '401': OpenApiResponse(description='Format is not specified'), '405': OpenApiResponse(description='Format is not available'), - }, tags=['projects'], versions=['v1']) + }, tags=['projects'], versions=['1.0']) @action(detail=True, methods=['GET'], serializer_class=LabeledDataSerializer) def annotations(self, request, pk): @@ -446,7 +446,7 @@ def annotations(self, request, pk): '200': OpenApiResponse(description='Download of file started'), '201': OpenApiResponse(description='Output backup file is ready for downloading'), '202': OpenApiResponse(description='Creating a backup file has been started'), - }, tags=['projects'], versions=['v1']) + }, tags=['projects'], versions=['1.0']) @action(methods=['GET'], detail=True, url_path='backup') def export_backup(self, request, pk=None): db_project = self.get_object() # force to call check_object_permissions @@ -456,7 +456,7 @@ def export_backup(self, request, pk=None): responses={ '201': OpenApiResponse(description='The project has been imported'), '202': OpenApiResponse(description='Importing a backup file has been started'), - }, tags=['projects'], versions=['v1']) + }, tags=['projects'], versions=['1.0']) @action(detail=False, methods=['POST'], url_path='backup') def import_backup(self, request, pk=None): return backup.import_project(request) @@ -585,30 +585,30 @@ class Meta: ], responses={ '200': TaskSerializer(many=True), - }, tags=['tasks'], versions=['v1'])) + }, tags=['tasks'], versions=['1.0'])) @extend_schema_view(create=extend_schema( summary='Method creates a new task in a database without any attached images and videos', responses={ '201': OpenApiResponse(description='The task has been created'), - }, tags=['tasks'], versions=['v1'])) + }, tags=['tasks'], versions=['1.0'])) @extend_schema_view(retrieve=extend_schema( summary='Method returns details of a specific task', - responses=TaskSerializer, tags=['tasks'], versions=['v1'])) + responses=TaskSerializer, tags=['tasks'], versions=['1.0'])) @extend_schema_view(update=extend_schema( summary='Method updates a task by id', responses={ '200': TaskSerializer, - }, tags=['tasks'], versions=['v1'])) + }, tags=['tasks'], versions=['1.0'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes a specific task, all attached jobs, annotations, and data', responses={ '204': OpenApiResponse('The task has been deleted'), - }, tags=['tasks'], versions=['v1'])) + }, tags=['tasks'], versions=['1.0'])) @extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in a task', responses={ '200': TaskSerializer, - }, tags=['tasks'], versions=['v1'])) + }, tags=['tasks'], versions=['1.0'])) class TaskViewSet(UploadMixin, viewsets.ModelViewSet): queryset = Task.objects.prefetch_related( Prefetch('label_set', queryset=models.Label.objects.order_by('id')), @@ -632,7 +632,7 @@ def get_queryset(self): responses={ '201': OpenApiResponse(description='The task has been imported'), '202': OpenApiResponse(description='Importing a backup file has been started'), - }, tags=['tasks'], versions=['v1']) + }, tags=['tasks'], versions=['1.0']) @action(detail=False, methods=['POST'], url_path='backup') def import_backup(self, request, pk=None): return backup.import_task(request) @@ -642,7 +642,7 @@ def import_backup(self, request, pk=None): '200': OpenApiResponse(description='Download of file started'), '201': OpenApiResponse(description='Output backup file is ready for downloading'), '202': OpenApiResponse(description='Creating a backup file has been started'), - }, tags=['tasks'], versions=['v1']) + }, tags=['tasks'], versions=['1.0']) @action(methods=['GET'], detail=True, url_path='backup') def export_backup(self, request, pk=None): db_task = self.get_object() # force to call check_object_permissions @@ -677,7 +677,7 @@ def perform_destroy(self, instance): @extend_schema(summary='Method returns a list of jobs for a specific task', responses={ '200': JobReadSerializer(many=True), - }, tags=['tasks'], versions=['v1']) + }, tags=['tasks'], versions=['1.0']) @action(detail=True, methods=['GET'], serializer_class=JobReadSerializer) def jobs(self, request, pk): self.get_object() # force to call check_object_permissions @@ -734,7 +734,7 @@ def upload_finished(self, request): ], responses={ '202': OpenApiResponse(), - }, tags=['tasks'], versions=['v1']) + }, tags=['tasks'], versions=['1.0']) @extend_schema(methods=['GET'], summary='Method returns data for a specific task', parameters=[ OpenApiParameter('type', location=OpenApiParameter.QUERY, required=True, @@ -748,7 +748,7 @@ def upload_finished(self, request): ], responses={ '200': OpenApiResponse('Data of a specific type'), - }, tags=['tasks'], versions=['v1']) + }, tags=['tasks'], versions=['1.0']) @action(detail=True, methods=['OPTIONS', 'POST', 'GET'], url_path=r'data/?$') def data(self, request, pk): db_task = self.get_object() # call check_object_permissions as well @@ -790,7 +790,7 @@ def data(self, request, pk): '201': OpenApiResponse(description='Annotations file is ready to download'), '202': OpenApiResponse(description='Dump of annotations has been started'), '405': OpenApiResponse(description='Format is not available'), - }, tags=['tasks'], versions=['v1']) + }, tags=['tasks'], versions=['1.0']) @extend_schema(methods=['PUT'], summary='Method allows to upload task annotations', parameters=[ OpenApiParameter('format', location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=False, @@ -800,16 +800,16 @@ def data(self, request, pk): '201': OpenApiResponse(description='Uploading has finished'), '202': OpenApiResponse(description='Uploading has been started'), '405': OpenApiResponse(description='Format is not available'), - }, tags=['tasks'], versions=['v1']) + }, tags=['tasks'], versions=['1.0']) @extend_schema(methods=['PATCH'], summary='Method performs a partial update of annotations in a specific task', parameters=[ OpenApiParameter('action', location=OpenApiParameter.QUERY, required=True, type=OpenApiTypes.STR, enum=['create', 'update', 'delete']), - ], tags=['tasks'], versions=['v1']) + ], tags=['tasks'], versions=['1.0']) @extend_schema(methods=['DELETE'], summary='Method deletes all annotations for a specific task', responses={ '204': OpenApiResponse(description='The annotation has been deleted'), - }, tags=['tasks'], versions=['v1']) + }, tags=['tasks'], versions=['1.0']) @action(detail=True, methods=['GET', 'DELETE', 'PUT', 'PATCH'], serializer_class=LabeledDataSerializer) def annotations(self, request, pk): @@ -865,7 +865,7 @@ def annotations(self, request, pk): summary='When task is being created the method returns information about a status of the creation process', responses={ '200': RqStatusSerializer, - }, tags=['tasks'], versions=['v1']) + }, tags=['tasks'], versions=['1.0']) @action(detail=True, methods=['GET'], serializer_class=RqStatusSerializer) def status(self, request, pk): self.get_object() # force to call check_object_permissions @@ -898,7 +898,7 @@ def _get_rq_response(queue, job_id): @extend_schema(summary='Method provides a meta information about media files which are related with the task', responses={ '200': DataMetaSerializer, - }, tags=['tasks'], versions=['v1']) + }, tags=['tasks'], versions=['1.0']) @action(detail=True, methods=['GET'], serializer_class=DataMetaSerializer, url_path='data/meta') def data_info(request, pk): @@ -942,7 +942,7 @@ def data_info(request, pk): '201': OpenApiResponse(description='Output file is ready for downloading'), '202': OpenApiResponse(description='Exporting has been started'), '405': OpenApiResponse(description='Format is not available'), - }, tags=['tasks'], versions=['v1']) + }, tags=['tasks'], versions=['1.0']) @action(detail=True, methods=['GET'], serializer_class=None, url_path='dataset') def dataset_export(self, request, pk): @@ -962,22 +962,22 @@ def dataset_export(self, request, pk): summary='Method returns details of a job', responses={ '200': JobReadSerializer, - }, tags=['jobs'], versions=['v1'])) + }, tags=['jobs'], versions=['1.0'])) @extend_schema_view(list=extend_schema( summary='Method returns a paginated list of jobs according to query parameters', responses={ '200': JobReadSerializer(many=True), - }, tags=['jobs'], versions=['v1'])) + }, tags=['jobs'], versions=['1.0'])) @extend_schema_view(update=extend_schema( summary='Method updates a job by id', responses={ '200': JobWriteSerializer, - }, tags=['jobs'], versions=['v1'])) + }, tags=['jobs'], versions=['1.0'])) @extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in a job', responses={ '200': JobWriteSerializer, - }, tags=['jobs'], versions=['v1'])) + }, tags=['jobs'], versions=['1.0'])) class JobViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin): queryset = Job.objects.all().order_by('id') @@ -999,13 +999,13 @@ def get_serializer_class(self): @extend_schema(methods=['GET'], summary='Method returns annotations for a specific job', responses={ '200': LabeledDataSerializer(many=True), - }, tags=['jobs'], versions=['v1']) + }, tags=['jobs'], versions=['1.0']) @extend_schema(methods=['PUT'], summary='Method performs an update of all annotations in a specific job', request=AnnotationFileSerializer, responses={ '201': OpenApiResponse(description='Uploading has finished'), '202': OpenApiResponse(description='Uploading has been started'), '405': OpenApiResponse(description='Format is not available'), - }, tags=['jobs'], versions=['v1']) + }, tags=['jobs'], versions=['1.0']) @extend_schema(methods=['PATCH'], summary='Method performs a partial update of annotations in a specific job', parameters=[ OpenApiParameter('action', location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, @@ -1014,11 +1014,11 @@ def get_serializer_class(self): responses={ #TODO '200': OpenApiResponse(), - }, tags=['jobs'], versions=['v1']) + }, tags=['jobs'], versions=['1.0']) @extend_schema(methods=['DELETE'], summary='Method deletes all annotations for a specific job', responses={ '204': OpenApiResponse('The annotation has been deleted'), - }, tags=['jobs'], versions=['v1']) + }, tags=['jobs'], versions=['1.0']) @action(detail=True, methods=['GET', 'DELETE', 'PUT', 'PATCH'], serializer_class=LabeledDataSerializer) def annotations(self, request, pk): @@ -1064,7 +1064,7 @@ def annotations(self, request, pk): summary='Method returns list of issues for the job', responses={ '200': IssueReadSerializer(many=True) - }, tags=['jobs'], versions=['v1']) + }, tags=['jobs'], versions=['1.0']) @action(detail=True, methods=['GET'], serializer_class=IssueReadSerializer) def issues(self, request, pk): db_job = self.get_object() @@ -1087,7 +1087,7 @@ def issues(self, request, pk): ], responses={ '200': OpenApiResponse(description='Data of a specific type'), - }, tags=['jobs'], versions=['v1']) + }, tags=['jobs'], versions=['1.0']) @action(detail=True, methods=['GET']) def data(self, request, pk): db_job = self.get_object() # call check_object_permissions as well @@ -1105,32 +1105,32 @@ def data(self, request, pk): summary='Method returns details of an issue', responses={ '200': IssueReadSerializer, - }, tags=['issues'], versions=['v1'])) + }, tags=['issues'], versions=['1.0'])) @extend_schema_view(list=extend_schema( summary='Method returns a paginated list of issues according to query parameters', responses={ '200': IssueReadSerializer(many=True), - }, tags=['issues'], versions=['v1'])) + }, tags=['issues'], versions=['1.0'])) @extend_schema_view(update=extend_schema( summary='Method updates an issue by id', responses={ '200': IssueWriteSerializer, - }, tags=['issues'], versions=['v1'])) + }, tags=['issues'], versions=['1.0'])) @extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in an issue', responses={ '200': IssueWriteSerializer, - }, tags=['issues'], versions=['v1'])) + }, tags=['issues'], versions=['1.0'])) @extend_schema_view(create=extend_schema( summary='Method creates an issue', responses={ '201': IssueWriteSerializer, - }, tags=['issues'], versions=['v1'])) + }, tags=['issues'], versions=['1.0'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes an issue', responses={ '204': OpenApiResponse('The issue has been deleted'), - }, tags=['issues'], versions=['v1'])) + }, tags=['issues'], versions=['1.0'])) class IssueViewSet(viewsets.ModelViewSet): queryset = Issue.objects.all().order_by('-id') http_method_names = ['get', 'post', 'patch', 'delete', 'options'] @@ -1155,7 +1155,7 @@ def perform_create(self, serializer): @extend_schema(summary='The action returns all comments of a specific issue', responses={ '200': CommentReadSerializer(many=True), - }, tags=['issues'], versions=['v1']) + }, tags=['issues'], versions=['1.0']) @action(detail=True, methods=['GET'], serializer_class=CommentReadSerializer) def comments(self, request, pk): db_issue = self.get_object() @@ -1169,32 +1169,32 @@ def comments(self, request, pk): summary='Method returns details of a comment', responses={ '200': CommentReadSerializer, - }, tags=['comments'], versions=['v1'])) + }, tags=['comments'], versions=['1.0'])) @extend_schema_view(list=extend_schema( summary='Method returns a paginated list of comments according to query parameters', responses={ '200':CommentReadSerializer(many=True), - }, tags=['comments'], versions=['v1'])) + }, tags=['comments'], versions=['1.0'])) @extend_schema_view(update=extend_schema( summary='Method updates a comment by id', responses={ '200': CommentWriteSerializer, - }, tags=['comments'], versions=['v1'])) + }, tags=['comments'], versions=['1.0'])) @extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in a comment', responses={ '200': CommentWriteSerializer, - }, tags=['comments'], versions=['v1'])) + }, tags=['comments'], versions=['1.0'])) @extend_schema_view(create=extend_schema( summary='Method creates a comment', responses={ '201': CommentWriteSerializer, - }, tags=['comments'], versions=['v1'])) + }, tags=['comments'], versions=['1.0'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes a comment', responses={ '204': OpenApiResponse('The comment has been deleted'), - }, tags=['comments'], versions=['v1'])) + }, tags=['comments'], versions=['1.0'])) class CommentViewSet(viewsets.ModelViewSet): queryset = Comment.objects.all().order_by('-id') http_method_names = ['get', 'post', 'patch', 'delete', 'options'] @@ -1234,7 +1234,7 @@ class Meta: serializers=[ UserSerializer, BasicUserSerializer, ], resource_type_field_name='type'), - }, tags=['users'], versions=['v1'])) + }, tags=['users'], versions=['1.0'])) @extend_schema_view(retrieve=extend_schema( summary='Method provides information of a specific user', responses={ @@ -1242,7 +1242,7 @@ class Meta: serializers=[ UserSerializer, BasicUserSerializer, ], resource_type_field_name='type'), - }, tags=['users'], versions=['v1'])) + }, tags=['users'], versions=['1.0'])) @extend_schema_view(partial_update=extend_schema( summary='Method updates chosen fields of a user', responses={ @@ -1250,12 +1250,12 @@ class Meta: serializers=[ UserSerializer, BasicUserSerializer, ], resource_type_field_name='type'), - }, tags=['users'], versions=['v1'])) + }, tags=['users'], versions=['1.0'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes a specific user from the server', responses={ '204': OpenApiResponse('The user has been deleted'), - }, tags=['users'], versions=['v1'])) + }, tags=['users'], versions=['1.0'])) class UserViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin): queryset = User.objects.prefetch_related('groups').all().order_by('id') @@ -1289,7 +1289,7 @@ def get_serializer_class(self): serializers=[ UserSerializer, BasicUserSerializer, ], resource_type_field_name='type'), - }, tags=['users'], versions=['v1']) + }, tags=['users'], versions=['1.0']) @action(detail=False, methods=['GET']) def self(self, request): """ @@ -1324,7 +1324,7 @@ class Meta: summary='Method returns details of a specific cloud storage', responses={ '200': CloudStorageReadSerializer, - }, tags=['cloud storages'], versions=['v1'])) + }, tags=['cloud storages'], versions=['1.0'])) @extend_schema_view(list=extend_schema( summary='Returns a paginated list of storages according to query parameters', parameters=[ @@ -1343,26 +1343,26 @@ class Meta: #field_inspectors=[RedefineDescriptionField] responses={ '200': CloudStorageReadSerializer(many=True), - }, tags=['cloud storages'], versions=['v1'])) + }, tags=['cloud storages'], versions=['1.0'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes a specific cloud storage', responses={ '204': OpenApiResponse(description='The cloud storage has been removed'), - }, tags=['cloud storages'], versions=['v1'])) + }, tags=['cloud storages'], versions=['1.0'])) @extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in a cloud storage instance', # FIXME #field_inspectors=[RedefineDescriptionField] responses={ '200': CloudStorageWriteSerializer, - }, tags=['cloud storages'], versions=['v1'])) + }, tags=['cloud storages'], versions=['1.0'])) @extend_schema_view(create=extend_schema( summary='Method creates a cloud storage with a specified characteristics', # FIXME #field_inspectors=[RedefineDescriptionField], responses={ '201': CloudStorageWriteSerializer, - }, tags=['cloud storages'], versions=['v1'])) + }, tags=['cloud storages'], versions=['1.0'])) class CloudStorageViewSet(viewsets.ModelViewSet): http_method_names = ['get', 'post', 'patch', 'delete'] queryset = CloudStorageModel.objects.all().prefetch_related('data').order_by('-id') @@ -1424,7 +1424,7 @@ def create(self, request, *args, **kwargs): ], responses={ '200': OpenApiResponse(description='A manifest content'), - }, tags=['cloud storages'], versions=['v1']) + }, tags=['cloud storages'], versions=['1.0']) @action(detail=True, methods=['GET'], url_path='content') def content(self, request, pk): storage = None @@ -1484,7 +1484,7 @@ def content(self, request, pk): @extend_schema(summary='Method returns a preview image from a cloud storage', responses={ '200': OpenApiResponse(description='Cloud Storage preview'), - }, tags=['cloud storages'], versions=['v1']) + }, tags=['cloud storages'], versions=['1.0']) @action(detail=True, methods=['GET'], url_path='preview') def preview(self, request, pk): storage = None @@ -1560,7 +1560,7 @@ def preview(self, request, pk): @extend_schema(summary='Method returns a cloud storage status', responses={ '200': OpenApiResponse(description='Cloud Storage status'), - }, tags=['cloud storages'], versions=['v1']) + }, tags=['cloud storages'], versions=['1.0']) @action(detail=True, methods=['GET'], url_path='status') def status(self, request, pk): try: diff --git a/cvat/apps/iam/views.py b/cvat/apps/iam/views.py index da57c45f5af..a6df29b005e 100644 --- a/cvat/apps/iam/views.py +++ b/cvat/apps/iam/views.py @@ -82,7 +82,7 @@ def __call__(self, request): 'url': serializers.CharField(), } ), - responses={'200': OpenApiResponse(description='text URL')}, tags=['auth'], versions=['v1'])) + responses={'200': OpenApiResponse(description='text URL')}, tags=['auth'], versions=['1.0'])) class SigningView(views.APIView): def post(self, request): diff --git a/cvat/apps/lambda_manager/views.py b/cvat/apps/lambda_manager/views.py index 8fd2d4e1e91..b6e48da30e9 100644 --- a/cvat/apps/lambda_manager/views.py +++ b/cvat/apps/lambda_manager/views.py @@ -553,9 +553,9 @@ def func_wrapper(*args, **kwargs): return wrap_response @extend_schema_view(retrieve=extend_schema( - summary='Method returns the information about the function', tags=['lambda'], versions=['v1'])) + summary='Method returns the information about the function', tags=['lambda'], versions=['1.0'])) @extend_schema_view(list=extend_schema( - summary='Method returns a list of functions', tags=['lambda'], versions=['v1'])) + summary='Method returns a list of functions', tags=['lambda'], versions=['1.0'])) class FunctionViewSet(viewsets.ViewSet): lookup_value_regex = '[a-zA-Z0-9_.-]+' lookup_field = 'func_id' @@ -589,14 +589,14 @@ def call(self, request, func_id): return lambda_func.invoke(db_task, request.data) @extend_schema_view(retrieve=extend_schema( - summary='Method returns the status of the request', tags=['lambda'], versions=['v1'])) + summary='Method returns the status of the request', tags=['lambda'], versions=['1.0'])) @extend_schema_view(list=extend_schema( - summary='Method returns a list of requests', tags=['lambda'], versions=['v1'])) + summary='Method returns a list of requests', tags=['lambda'], versions=['1.0'])) #TODO @extend_schema_view(create=extend_schema( - summary='Method calls the function', tags=['lambda'], versions=['v1'])) + summary='Method calls the function', tags=['lambda'], versions=['1.0'])) @extend_schema_view(delete=extend_schema( - summary='Method cancels the request', tags=['lambda'], versions=['v1'])) + summary='Method cancels the request', tags=['lambda'], versions=['1.0'])) class RequestViewSet(viewsets.ViewSet): @return_response() def list(self, request): diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index f23d5f67076..a21e1eef342 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -23,32 +23,32 @@ summary='Method returns details of an organization', responses={ '200': OrganizationReadSerializer, - }, tags=['organizations'])) # versions=['v1'] + }, tags=['organizations'], versions=['1.0'])) @extend_schema_view(list=extend_schema( summary='Method returns a paginated list of organizatins according to query parameters', responses={ '200': OrganizationReadSerializer(many=True), - }, tags=['organizations']))# versions=['v1'] + }, tags=['organizations'], versions=['1.0'])) @extend_schema_view(update=extend_schema( summary='Method updates an organization by id', responses={ '200': OrganizationWriteSerializer, - }, tags=['organizations'])) # versions=['v1'] + }, tags=['organizations'], versions=['1.0'])) @extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in an organization', responses={ '200': OrganizationWriteSerializer, - }, tags=['organizations'])) # versions=['v1'] + }, tags=['organizations'], versions=['1.0'])) @extend_schema_view(create=extend_schema( summary='Method creates an organization', responses={ '201': OrganizationWriteSerializer, - }, tags=['organizations'])) # versions=['v1'] + }, tags=['organizations'], versions=['1.0'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes an organization', responses={ '204': OpenApiResponse('The organization has been deleted'), - }, tags=['organizations'])) # versions=['v1'] + }, tags=['organizations'], versions=['1.0'])) class OrganizationViewSet(viewsets.ModelViewSet): queryset = Organization.objects.all() ordering = ['-id'] @@ -82,27 +82,27 @@ class Meta: summary='Method returns details of a membership', responses={ '200': MembershipReadSerializer, - }, tags=['memberships'], versions=['v1'])) + }, tags=['memberships'], versions=['1.0'])) @extend_schema_view(list=extend_schema( summary='Method returns a paginated list of memberships according to query parameters', responses={ '200': MembershipReadSerializer(many=True), - }, tags=['memberships'], versions=['v1'])) + }, tags=['memberships'], versions=['1.0'])) @extend_schema_view(update=extend_schema( summary='Method updates a membership by id', responses={ '200': MembershipWriteSerializer, - }, tags=['memberships'], versions=['v1'])) + }, tags=['memberships'], versions=['1.0'])) @extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in a membership', responses={ '200': MembershipWriteSerializer, - }, tags=['memberships'], versions=['v1'])) + }, tags=['memberships'], versions=['1.0'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes a membership', responses={ '204': OpenApiResponse('The membership has been deleted'), - }, tags=['memberships'], versions=['v1'])) + }, tags=['memberships'], versions=['1.0'])) class MembershipViewSet(mixins.RetrieveModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet): queryset = Membership.objects.all() @@ -126,32 +126,32 @@ def get_queryset(self): summary='Method returns details of an invitation', responses={ '200': InvitationReadSerializer, - }, tags=['invitations'], versions=['v1'])) + }, tags=['invitations'], versions=['1.0'])) @extend_schema_view(list=extend_schema( summary='Method returns a paginated list of invitations according to query parameters', responses={ '200': InvitationReadSerializer(many=True), - }, tags=['invitations'], versions=['v1'])) + }, tags=['invitations'], versions=['1.0'])) @extend_schema_view(update=extend_schema( summary='Method updates an invitation by id', responses={ '200': InvitationWriteSerializer, - }, tags=['invitations'], versions=['v1'])) + }, tags=['invitations'], versions=['1.0'])) @extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in an invitation', responses={ '200': InvitationWriteSerializer, - }, tags=['invitations'], versions=['v1'])) + }, tags=['invitations'], versions=['1.0'])) @extend_schema_view(create=extend_schema( summary='Method creates an invitation', responses={ '201': InvitationWriteSerializer, - }, tags=['invitations'], versions=['v1'])) + }, tags=['invitations'], versions=['1.0'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes an invitation', responses={ '204': OpenApiResponse('The invitation has been deleted'), - }, tags=['invitations'], versions=['v1'])) + }, tags=['invitations'], versions=['1.0'])) class InvitationViewSet(viewsets.ModelViewSet): queryset = Invitation.objects.all() ordering = ['-created_date'] diff --git a/cvat/apps/restrictions/views.py b/cvat/apps/restrictions/views.py index 0ef15ef8058..549bdd9534b 100644 --- a/cvat/apps/restrictions/views.py +++ b/cvat/apps/restrictions/views.py @@ -26,7 +26,7 @@ def get_serializer(self, *args, **kwargs): @staticmethod @extend_schema(summary='Method provides user agreements that the user must accept to register', responses={'200': UserAgreementSerializer}, - tags=['restrictions'], versions=['v1']) + tags=['restrictions'], versions=['1.0']) @action(detail=False, methods=['GET'], serializer_class=UserAgreementSerializer, url_path='user-agreements') def user_agreements(request): user_agreements = settings.RESTRICTIONS['user_agreements'] @@ -37,7 +37,7 @@ def user_agreements(request): @staticmethod @extend_schema(summary='Method provides CVAT terms of use', responses={'200': OpenApiResponse(description='CVAT terms of use')}, - tags=['restrictions'], versions=['v1']) + tags=['restrictions'], versions=['1.0']) @action(detail=False, methods=['GET'], renderer_classes=(TemplateHTMLRenderer,), url_path='terms-of-use') def terms_of_use(request): diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 9e2aa511c6e..e6f66d62150 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -520,7 +520,7 @@ def add_ssh_keys(): SPECTACULAR_SETTINGS = { 'TITLE': 'CVAT REST API', 'DESCRIPTION': 'REST API for Computer Vision Annotation Tool (CVAT)', - 'VERSION': 'v1', + 'VERSION': '1.0', 'CONTACT': { 'name': 'Nikita Manovich', 'url': 'https://github.com/nmanovic', @@ -532,7 +532,7 @@ def add_ssh_keys(): }, 'SERVE_PUBLIC': True, 'SCHEMA_COERCE_PATH_PK_SUFFIX': True, - 'SCHEMA_PATH_PREFIX': r'/api/v[0-9]', + 'SCHEMA_PATH_PREFIX': '/api', 'SCHEMA_PATH_PREFIX_TRIM': True, 'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'], 'SERVERS': [ From 08d53b9fcce21dc5b0c55166129ce44ec576a72d Mon Sep 17 00:00:00 2001 From: Maria Khrustaleva Date: Thu, 27 Jan 2022 13:34:15 +0300 Subject: [PATCH 12/22] remove unused imports --- cvat/apps/engine/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index f372ab9fbc8..d34846235aa 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -23,11 +23,10 @@ from django.http import HttpResponse, HttpResponseNotFound, HttpResponseBadRequest from django.utils import timezone from django_filters import rest_framework as filters -from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import ( - OpenApiExample, OpenApiParameter, OpenApiResponse, PolymorphicProxySerializer, + OpenApiParameter, OpenApiResponse, PolymorphicProxySerializer, extend_schema_view, extend_schema ) From 9e82fc033d34044b0356a28bee410c34546e218c Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 4 Feb 2022 11:38:29 +0300 Subject: [PATCH 13/22] Update settings: fix version & servers & remove URL_FORMAT_OVERRIDE --- cvat/settings/base.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/cvat/settings/base.py b/cvat/settings/base.py index e7cd7934a8b..686d4d948c0 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -168,8 +168,6 @@ def add_ssh_keys(): 'rest_framework.filters.OrderingFilter', 'cvat.apps.iam.filters.OrganizationFilterBackend'), - # Disable default handling of the 'format' query parameter by REST framework - 'URL_FORMAT_OVERRIDE': 'scheme', 'DEFAULT_THROTTLE_CLASSES': [ 'rest_framework.throttling.AnonRateThrottle', ], @@ -500,7 +498,10 @@ def add_ssh_keys(): SPECTACULAR_SETTINGS = { 'TITLE': 'CVAT REST API', 'DESCRIPTION': 'REST API for Computer Vision Annotation Tool (CVAT)', - 'VERSION': '1.0', + # Statically set schema version. May also be an empty string. When used together with + # view versioning, will become '0.0.0 (v2)' for 'v2' versioned requests. + # Set VERSION to None if only the request version should be rendered. + 'VERSION': None, 'CONTACT': { 'name': 'Nikita Manovich', 'url': 'https://github.com/nmanovic', @@ -516,8 +517,9 @@ def add_ssh_keys(): 'SCHEMA_PATH_PREFIX_TRIM': True, 'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'], 'SERVERS': [ + # https://swagger.io/specification/#schema-object { - 'url': 'https://localhost:7000/', + 'url': '{protocol}://localhost:7000/api/', 'description': 'Development server', 'variables': { 'protocol': { @@ -525,13 +527,25 @@ def add_ssh_keys(): 'default': 'http', } }, + }, + { + 'url': '{protocol}://localhost:8080/api/', + 'description': 'Production server', + 'variables': { + 'protocol': { + 'enum': ['http', 'https'], + 'default': 'http', + } + }, } ], + # https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/ 'SWAGGER_UI_SETTINGS': { 'deepLinking': True, 'displayOperationId': True, 'displayRequestDuration': True, 'filter': True, + 'showExtensions': True, }, 'TOS': 'https://www.google.com/policies/terms/', 'EXTERNAL_DOCS': { From 631fcfff03887d77e4720c54ff86dd792056e644 Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 4 Feb 2022 11:39:08 +0300 Subject: [PATCH 14/22] Update version --- cvat/apps/engine/urls.py | 2 +- cvat/apps/engine/views.py | 142 +++++++++++++++--------------- cvat/apps/iam/views.py | 2 +- cvat/apps/lambda_manager/views.py | 12 +-- cvat/apps/organizations/views.py | 34 +++---- cvat/apps/restrictions/views.py | 4 +- 6 files changed, 98 insertions(+), 98 deletions(-) diff --git a/cvat/apps/engine/urls.py b/cvat/apps/engine/urls.py index 9612f0c9655..8d18144b49a 100644 --- a/cvat/apps/engine/urls.py +++ b/cvat/apps/engine/urls.py @@ -32,7 +32,7 @@ query_string=True)), # documentation for API - path('api/schema/', SpectacularAPIView.as_view(api_version='1.0'), name='schema'), + path('api/schema/', SpectacularAPIView.as_view(api_version='2.0'), name='schema'), path('api/swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger'), path('api/docs/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index f7bdb45ce64..9c935caaab1 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -87,7 +87,7 @@ def get_serializer(self, *args, **kwargs): responses={ '200': AboutSerializer, }, - tags=['server'], versions=['1.0']) + tags=['server'], versions=['2.0']) @action(detail=False, methods=['GET'], serializer_class=AboutSerializer) def about(request): from cvat import __version__ as cvat_version @@ -112,7 +112,7 @@ def about(request): request=ExceptionSerializer, responses={ '201': ExceptionSerializer, }, - tags=['server'], versions=['1.0']) + tags=['server'], versions=['2.0']) @action(detail=False, methods=['POST'], serializer_class=ExceptionSerializer) def exception(request): serializer = ExceptionSerializer(data=request.data) @@ -139,7 +139,7 @@ def exception(request): request=LogEventSerializer(many=True), responses={ '201': LogEventSerializer(many=True), - }, tags=['server'], versions=['1.0']) + }, tags=['server'], versions=['2.0']) @action(detail=False, methods=['POST'], serializer_class=LogEventSerializer) def logs(request): serializer = LogEventSerializer(many=True, data=request.data) @@ -166,7 +166,7 @@ def logs(request): ], responses={ '200' : FileInfoSerializer(many=True) - }, tags=['server'], versions=['1.0']) + }, tags=['server'], versions=['2.0']) @action(detail=False, methods=['GET'], serializer_class=FileInfoSerializer) def share(request): param = request.query_params.get('directory', '/') @@ -199,7 +199,7 @@ def share(request): summary='Method provides the list of supported annotations formats', responses={ '200': DatasetFormatsSerializer, - }, tags=['server'], versions=['1.0']) + }, tags=['server'], versions=['2.0']) @action(detail=False, methods=['GET'], url_path='annotation/formats') def annotation_formats(request): data = dm.views.get_all_formats() @@ -210,7 +210,7 @@ def annotation_formats(request): summary='Method provides allowed plugins', responses={ '200': PluginsSerializer, - }, tags=['server'], versions=['1.0']) + }, tags=['server'], versions=['2.0']) @action(detail=False, methods=['GET'], url_path='plugins', serializer_class=PluginsSerializer) def plugins(request): response = { @@ -255,27 +255,27 @@ class Meta: serializers=[ ProjectSerializer, ProjectSearchSerializer, ], resource_type_field_name='type', many=True), - }, tags=['projects'], versions=['1.0'])) + }, tags=['projects'], versions=['2.0'])) @extend_schema_view(create=extend_schema( summary='Method creates a new project', responses={ '201': ProjectSerializer, - }, tags=['projects'], versions=['1.0'])) + }, tags=['projects'], versions=['2.0'])) @extend_schema_view(retrieve=extend_schema( summary='Method returns details of a specific project', responses={ '200': ProjectSerializer, - }, tags=['projects'], versions=['1.0'])) + }, tags=['projects'], versions=['2.0'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes a specific project', responses={ '204': OpenApiResponse(description='The project has been deleted'), - }, tags=['projects'], versions=['1.0'])) + }, tags=['projects'], versions=['2.0'])) @extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in a project', responses={ '200': ProjectSerializer, - }, tags=['projects'], versions=['1.0'])) + }, tags=['projects'], versions=['2.0'])) class ProjectViewSet(viewsets.ModelViewSet): queryset = models.Project.objects.prefetch_related(Prefetch('label_set', queryset=models.Label.objects.order_by('id') @@ -311,7 +311,7 @@ def perform_create(self, serializer): summary='Method returns information of the tasks of the project with the selected id', responses={ '200': TaskSerializer(many=True), - }, tags=['projects'], versions=['1.0']) + }, tags=['projects'], versions=['2.0']) @action(detail=True, methods=['GET'], serializer_class=TaskSerializer) def tasks(self, request, pk): self.get_object() # force to call check_object_permissions @@ -343,7 +343,7 @@ def tasks(self, request, pk): '201': OpenApiResponse(description='Output file is ready for downloading'), '202': OpenApiResponse(description='Exporting has been started'), '405': OpenApiResponse(description='Format is not available'), - }, tags=['projects'], versions=['1.0']) + }, tags=['projects'], versions=['2.0']) @extend_schema(methods=['POST'], summary='Import dataset in specific format as a project', parameters=[ OpenApiParameter('format', description='Desired dataset format name\n' @@ -354,7 +354,7 @@ def tasks(self, request, pk): '202': OpenApiResponse(description='Exporting has been started'), '400': OpenApiResponse(description='Failed to import dataset'), '405': OpenApiResponse(description='Format is not available'), - }, tags=['projects'], versions=['1.0']) + }, tags=['projects'], versions=['2.0']) @action(detail=True, methods=['GET', 'POST'], serializer_class=None, url_path='dataset') def dataset(self, request, pk): @@ -423,7 +423,7 @@ def dataset(self, request, pk): '202': OpenApiResponse(description='Dump of annotations has been started'), '401': OpenApiResponse(description='Format is not specified'), '405': OpenApiResponse(description='Format is not available'), - }, tags=['projects'], versions=['1.0']) + }, tags=['projects'], versions=['2.0']) @action(detail=True, methods=['GET'], serializer_class=LabeledDataSerializer) def annotations(self, request, pk): @@ -446,7 +446,7 @@ def annotations(self, request, pk): '200': OpenApiResponse(description='Download of file started'), '201': OpenApiResponse(description='Output backup file is ready for downloading'), '202': OpenApiResponse(description='Creating a backup file has been started'), - }, tags=['projects'], versions=['1.0']) + }, tags=['projects'], versions=['2.0']) @action(methods=['GET'], detail=True, url_path='backup') def export_backup(self, request, pk=None): db_project = self.get_object() # force to call check_object_permissions @@ -456,7 +456,7 @@ def export_backup(self, request, pk=None): responses={ '201': OpenApiResponse(description='The project has been imported'), '202': OpenApiResponse(description='Importing a backup file has been started'), - }, tags=['projects'], versions=['1.0']) + }, tags=['projects'], versions=['2.0']) @action(detail=False, methods=['POST'], url_path='backup') def import_backup(self, request, pk=None): return backup.import_project(request) @@ -585,30 +585,30 @@ class Meta: ], responses={ '200': TaskSerializer(many=True), - }, tags=['tasks'], versions=['1.0'])) + }, tags=['tasks'], versions=['2.0'])) @extend_schema_view(create=extend_schema( summary='Method creates a new task in a database without any attached images and videos', responses={ '201': OpenApiResponse(description='The task has been created'), - }, tags=['tasks'], versions=['1.0'])) + }, tags=['tasks'], versions=['2.0'])) @extend_schema_view(retrieve=extend_schema( summary='Method returns details of a specific task', - responses=TaskSerializer, tags=['tasks'], versions=['1.0'])) + responses=TaskSerializer, tags=['tasks'], versions=['2.0'])) @extend_schema_view(update=extend_schema( summary='Method updates a task by id', responses={ '200': TaskSerializer, - }, tags=['tasks'], versions=['1.0'])) + }, tags=['tasks'], versions=['2.0'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes a specific task, all attached jobs, annotations, and data', responses={ '204': OpenApiResponse('The task has been deleted'), - }, tags=['tasks'], versions=['1.0'])) + }, tags=['tasks'], versions=['2.0'])) @extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in a task', responses={ '200': TaskSerializer, - }, tags=['tasks'], versions=['1.0'])) + }, tags=['tasks'], versions=['2.0'])) class TaskViewSet(UploadMixin, viewsets.ModelViewSet): queryset = Task.objects.prefetch_related( Prefetch('label_set', queryset=models.Label.objects.order_by('id')), @@ -633,7 +633,7 @@ def get_queryset(self): responses={ '201': OpenApiResponse(description='The task has been imported'), '202': OpenApiResponse(description='Importing a backup file has been started'), - }, tags=['tasks'], versions=['1.0']) + }, tags=['tasks'], versions=['2.0']) @action(detail=False, methods=['POST'], url_path='backup') def import_backup(self, request, pk=None): return backup.import_task(request) @@ -643,7 +643,7 @@ def import_backup(self, request, pk=None): '200': OpenApiResponse(description='Download of file started'), '201': OpenApiResponse(description='Output backup file is ready for downloading'), '202': OpenApiResponse(description='Creating a backup file has been started'), - }, tags=['tasks'], versions=['1.0']) + }, tags=['tasks'], versions=['2.0']) @action(methods=['GET'], detail=True, url_path='backup') def export_backup(self, request, pk=None): db_task = self.get_object() # force to call check_object_permissions @@ -679,7 +679,7 @@ def perform_destroy(self, instance): @extend_schema(summary='Method returns a list of jobs for a specific task', responses={ '200': JobReadSerializer(many=True), - }, tags=['tasks'], versions=['1.0']) + }, tags=['tasks'], versions=['2.0']) @action(detail=True, methods=['GET'], serializer_class=JobReadSerializer) def jobs(self, request, pk): self.get_object() # force to call check_object_permissions @@ -736,7 +736,7 @@ def upload_finished(self, request): ], responses={ '202': OpenApiResponse(), - }, tags=['tasks'], versions=['1.0']) + }, tags=['tasks'], versions=['2.0']) @extend_schema(methods=['GET'], summary='Method returns data for a specific task', parameters=[ OpenApiParameter('type', location=OpenApiParameter.QUERY, required=True, @@ -750,7 +750,7 @@ def upload_finished(self, request): ], responses={ '200': OpenApiResponse('Data of a specific type'), - }, tags=['tasks'], versions=['1.0']) + }, tags=['tasks'], versions=['2.0']) @action(detail=True, methods=['OPTIONS', 'POST', 'GET'], url_path=r'data/?$') def data(self, request, pk): db_task = self.get_object() # call check_object_permissions as well @@ -792,7 +792,7 @@ def data(self, request, pk): '201': OpenApiResponse(description='Annotations file is ready to download'), '202': OpenApiResponse(description='Dump of annotations has been started'), '405': OpenApiResponse(description='Format is not available'), - }, tags=['tasks'], versions=['1.0']) + }, tags=['tasks'], versions=['2.0']) @extend_schema(methods=['PUT'], summary='Method allows to upload task annotations', parameters=[ OpenApiParameter('format', location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=False, @@ -802,16 +802,16 @@ def data(self, request, pk): '201': OpenApiResponse(description='Uploading has finished'), '202': OpenApiResponse(description='Uploading has been started'), '405': OpenApiResponse(description='Format is not available'), - }, tags=['tasks'], versions=['1.0']) + }, tags=['tasks'], versions=['2.0']) @extend_schema(methods=['PATCH'], summary='Method performs a partial update of annotations in a specific task', parameters=[ OpenApiParameter('action', location=OpenApiParameter.QUERY, required=True, type=OpenApiTypes.STR, enum=['create', 'update', 'delete']), - ], tags=['tasks'], versions=['1.0']) + ], tags=['tasks'], versions=['2.0']) @extend_schema(methods=['DELETE'], summary='Method deletes all annotations for a specific task', responses={ '204': OpenApiResponse(description='The annotation has been deleted'), - }, tags=['tasks'], versions=['1.0']) + }, tags=['tasks'], versions=['2.0']) @action(detail=True, methods=['GET', 'DELETE', 'PUT', 'PATCH'], serializer_class=LabeledDataSerializer) def annotations(self, request, pk): @@ -867,7 +867,7 @@ def annotations(self, request, pk): summary='When task is being created the method returns information about a status of the creation process', responses={ '200': RqStatusSerializer, - }, tags=['tasks'], versions=['1.0']) + }, tags=['tasks'], versions=['2.0']) @action(detail=True, methods=['GET'], serializer_class=RqStatusSerializer) def status(self, request, pk): self.get_object() # force to call check_object_permissions @@ -900,7 +900,7 @@ def _get_rq_response(queue, job_id): @extend_schema(summary='Method provides a meta information about media files which are related with the task', responses={ '200': DataMetaSerializer, - }, tags=['tasks'], versions=['1.0']) + }, tags=['tasks'], versions=['2.0']) @action(detail=True, methods=['GET'], serializer_class=DataMetaSerializer, url_path='data/meta') def data_info(request, pk): @@ -944,7 +944,7 @@ def data_info(request, pk): '201': OpenApiResponse(description='Output file is ready for downloading'), '202': OpenApiResponse(description='Exporting has been started'), '405': OpenApiResponse(description='Format is not available'), - }, tags=['tasks'], versions=['1.0']) + }, tags=['tasks'], versions=['2.0']) @action(detail=True, methods=['GET'], serializer_class=None, url_path='dataset') def dataset_export(self, request, pk): @@ -976,22 +976,22 @@ class Meta: summary='Method returns details of a job', responses={ '200': JobReadSerializer, - }, tags=['jobs'], versions=['1.0'])) + }, tags=['jobs'], versions=['2.0'])) @extend_schema_view(list=extend_schema( summary='Method returns a paginated list of jobs according to query parameters', responses={ '200': JobReadSerializer(many=True), - }, tags=['jobs'], versions=['1.0'])) + }, tags=['jobs'], versions=['2.0'])) @extend_schema_view(update=extend_schema( summary='Method updates a job by id', responses={ '200': JobWriteSerializer, - }, tags=['jobs'], versions=['1.0'])) + }, tags=['jobs'], versions=['2.0'])) @extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in a job', responses={ '200': JobWriteSerializer, - }, tags=['jobs'], versions=['1.0'])) + }, tags=['jobs'], versions=['2.0'])) class JobViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin): queryset = Job.objects.all().order_by('id') @@ -1015,13 +1015,13 @@ def get_serializer_class(self): @extend_schema(methods=['GET'], summary='Method returns annotations for a specific job', responses={ '200': LabeledDataSerializer(many=True), - }, tags=['jobs'], versions=['1.0']) + }, tags=['jobs'], versions=['2.0']) @extend_schema(methods=['PUT'], summary='Method performs an update of all annotations in a specific job', request=AnnotationFileSerializer, responses={ '201': OpenApiResponse(description='Uploading has finished'), '202': OpenApiResponse(description='Uploading has been started'), '405': OpenApiResponse(description='Format is not available'), - }, tags=['jobs'], versions=['1.0']) + }, tags=['jobs'], versions=['2.0']) @extend_schema(methods=['PATCH'], summary='Method performs a partial update of annotations in a specific job', parameters=[ OpenApiParameter('action', location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, @@ -1030,11 +1030,11 @@ def get_serializer_class(self): responses={ #TODO '200': OpenApiResponse(), - }, tags=['jobs'], versions=['1.0']) + }, tags=['jobs'], versions=['2.0']) @extend_schema(methods=['DELETE'], summary='Method deletes all annotations for a specific job', responses={ '204': OpenApiResponse('The annotation has been deleted'), - }, tags=['jobs'], versions=['1.0']) + }, tags=['jobs'], versions=['2.0']) @action(detail=True, methods=['GET', 'DELETE', 'PUT', 'PATCH'], serializer_class=LabeledDataSerializer) def annotations(self, request, pk): @@ -1080,7 +1080,7 @@ def annotations(self, request, pk): summary='Method returns list of issues for the job', responses={ '200': IssueReadSerializer(many=True) - }, tags=['jobs'], versions=['1.0']) + }, tags=['jobs'], versions=['2.0']) @action(detail=True, methods=['GET'], serializer_class=IssueReadSerializer) def issues(self, request, pk): db_job = self.get_object() @@ -1103,7 +1103,7 @@ def issues(self, request, pk): ], responses={ '200': OpenApiResponse(description='Data of a specific type'), - }, tags=['jobs'], versions=['1.0']) + }, tags=['jobs'], versions=['2.0']) @action(detail=True, methods=['GET']) def data(self, request, pk): db_job = self.get_object() # call check_object_permissions as well @@ -1121,32 +1121,32 @@ def data(self, request, pk): summary='Method returns details of an issue', responses={ '200': IssueReadSerializer, - }, tags=['issues'], versions=['1.0'])) + }, tags=['issues'], versions=['2.0'])) @extend_schema_view(list=extend_schema( summary='Method returns a paginated list of issues according to query parameters', responses={ '200': IssueReadSerializer(many=True), - }, tags=['issues'], versions=['1.0'])) + }, tags=['issues'], versions=['2.0'])) @extend_schema_view(update=extend_schema( summary='Method updates an issue by id', responses={ '200': IssueWriteSerializer, - }, tags=['issues'], versions=['1.0'])) + }, tags=['issues'], versions=['2.0'])) @extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in an issue', responses={ '200': IssueWriteSerializer, - }, tags=['issues'], versions=['1.0'])) + }, tags=['issues'], versions=['2.0'])) @extend_schema_view(create=extend_schema( summary='Method creates an issue', responses={ '201': IssueWriteSerializer, - }, tags=['issues'], versions=['1.0'])) + }, tags=['issues'], versions=['2.0'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes an issue', responses={ '204': OpenApiResponse('The issue has been deleted'), - }, tags=['issues'], versions=['1.0'])) + }, tags=['issues'], versions=['2.0'])) class IssueViewSet(viewsets.ModelViewSet): queryset = Issue.objects.all().order_by('-id') http_method_names = ['get', 'post', 'patch', 'delete', 'options'] @@ -1172,7 +1172,7 @@ def perform_create(self, serializer): @extend_schema(summary='The action returns all comments of a specific issue', responses={ '200': CommentReadSerializer(many=True), - }, tags=['issues'], versions=['1.0']) + }, tags=['issues'], versions=['2.0']) @action(detail=True, methods=['GET'], serializer_class=CommentReadSerializer) def comments(self, request, pk): db_issue = self.get_object() @@ -1186,32 +1186,32 @@ def comments(self, request, pk): summary='Method returns details of a comment', responses={ '200': CommentReadSerializer, - }, tags=['comments'], versions=['1.0'])) + }, tags=['comments'], versions=['2.0'])) @extend_schema_view(list=extend_schema( summary='Method returns a paginated list of comments according to query parameters', responses={ '200':CommentReadSerializer(many=True), - }, tags=['comments'], versions=['1.0'])) + }, tags=['comments'], versions=['2.0'])) @extend_schema_view(update=extend_schema( summary='Method updates a comment by id', responses={ '200': CommentWriteSerializer, - }, tags=['comments'], versions=['1.0'])) + }, tags=['comments'], versions=['2.0'])) @extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in a comment', responses={ '200': CommentWriteSerializer, - }, tags=['comments'], versions=['1.0'])) + }, tags=['comments'], versions=['2.0'])) @extend_schema_view(create=extend_schema( summary='Method creates a comment', responses={ '201': CommentWriteSerializer, - }, tags=['comments'], versions=['1.0'])) + }, tags=['comments'], versions=['2.0'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes a comment', responses={ '204': OpenApiResponse('The comment has been deleted'), - }, tags=['comments'], versions=['1.0'])) + }, tags=['comments'], versions=['2.0'])) class CommentViewSet(viewsets.ModelViewSet): queryset = Comment.objects.all().order_by('-id') http_method_names = ['get', 'post', 'patch', 'delete', 'options'] @@ -1252,7 +1252,7 @@ class Meta: serializers=[ UserSerializer, BasicUserSerializer, ], resource_type_field_name='type'), - }, tags=['users'], versions=['1.0'])) + }, tags=['users'], versions=['2.0'])) @extend_schema_view(retrieve=extend_schema( summary='Method provides information of a specific user', responses={ @@ -1260,7 +1260,7 @@ class Meta: serializers=[ UserSerializer, BasicUserSerializer, ], resource_type_field_name='type'), - }, tags=['users'], versions=['1.0'])) + }, tags=['users'], versions=['2.0'])) @extend_schema_view(partial_update=extend_schema( summary='Method updates chosen fields of a user', responses={ @@ -1268,12 +1268,12 @@ class Meta: serializers=[ UserSerializer, BasicUserSerializer, ], resource_type_field_name='type'), - }, tags=['users'], versions=['1.0'])) + }, tags=['users'], versions=['2.0'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes a specific user from the server', responses={ '204': OpenApiResponse('The user has been deleted'), - }, tags=['users'], versions=['1.0'])) + }, tags=['users'], versions=['2.0'])) class UserViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin): queryset = User.objects.prefetch_related('groups').all().order_by('id') @@ -1308,7 +1308,7 @@ def get_serializer_class(self): serializers=[ UserSerializer, BasicUserSerializer, ], resource_type_field_name='type'), - }, tags=['users'], versions=['1.0']) + }, tags=['users'], versions=['2.0']) @action(detail=False, methods=['GET']) def self(self, request): """ @@ -1343,7 +1343,7 @@ class Meta: summary='Method returns details of a specific cloud storage', responses={ '200': CloudStorageReadSerializer, - }, tags=['cloud storages'], versions=['1.0'])) + }, tags=['cloud storages'], versions=['2.0'])) @extend_schema_view(list=extend_schema( summary='Returns a paginated list of storages according to query parameters', parameters=[ @@ -1362,26 +1362,26 @@ class Meta: #field_inspectors=[RedefineDescriptionField] responses={ '200': CloudStorageReadSerializer(many=True), - }, tags=['cloud storages'], versions=['1.0'])) + }, tags=['cloud storages'], versions=['2.0'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes a specific cloud storage', responses={ '204': OpenApiResponse(description='The cloud storage has been removed'), - }, tags=['cloud storages'], versions=['1.0'])) + }, tags=['cloud storages'], versions=['2.0'])) @extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in a cloud storage instance', # FIXME #field_inspectors=[RedefineDescriptionField] responses={ '200': CloudStorageWriteSerializer, - }, tags=['cloud storages'], versions=['1.0'])) + }, tags=['cloud storages'], versions=['2.0'])) @extend_schema_view(create=extend_schema( summary='Method creates a cloud storage with a specified characteristics', # FIXME #field_inspectors=[RedefineDescriptionField], responses={ '201': CloudStorageWriteSerializer, - }, tags=['cloud storages'], versions=['1.0'])) + }, tags=['cloud storages'], versions=['2.0'])) class CloudStorageViewSet(viewsets.ModelViewSet): http_method_names = ['get', 'post', 'patch', 'delete'] queryset = CloudStorageModel.objects.all().prefetch_related('data').order_by('-id') @@ -1443,7 +1443,7 @@ def create(self, request, *args, **kwargs): ], responses={ '200': OpenApiResponse(description='A manifest content'), - }, tags=['cloud storages'], versions=['1.0']) + }, tags=['cloud storages'], versions=['2.0']) @action(detail=True, methods=['GET'], url_path='content') def content(self, request, pk): storage = None @@ -1503,7 +1503,7 @@ def content(self, request, pk): @extend_schema(summary='Method returns a preview image from a cloud storage', responses={ '200': OpenApiResponse(description='Cloud Storage preview'), - }, tags=['cloud storages'], versions=['1.0']) + }, tags=['cloud storages'], versions=['2.0']) @action(detail=True, methods=['GET'], url_path='preview') def preview(self, request, pk): storage = None @@ -1579,7 +1579,7 @@ def preview(self, request, pk): @extend_schema(summary='Method returns a cloud storage status', responses={ '200': OpenApiResponse(description='Cloud Storage status'), - }, tags=['cloud storages'], versions=['1.0']) + }, tags=['cloud storages'], versions=['2.0']) @action(detail=True, methods=['GET'], url_path='status') def status(self, request, pk): try: diff --git a/cvat/apps/iam/views.py b/cvat/apps/iam/views.py index 05f8f71a38e..20a8b1ca494 100644 --- a/cvat/apps/iam/views.py +++ b/cvat/apps/iam/views.py @@ -88,7 +88,7 @@ def __call__(self, request): 'url': serializers.CharField(), } ), - responses={'200': OpenApiResponse(description='text URL')}, tags=['auth'], versions=['1.0'])) + responses={'200': OpenApiResponse(description='text URL')}, tags=['auth'], versions=['2.0'])) class SigningView(views.APIView): def post(self, request): diff --git a/cvat/apps/lambda_manager/views.py b/cvat/apps/lambda_manager/views.py index d2af769ce7e..b9312f1ef79 100644 --- a/cvat/apps/lambda_manager/views.py +++ b/cvat/apps/lambda_manager/views.py @@ -553,9 +553,9 @@ def func_wrapper(*args, **kwargs): return wrap_response @extend_schema_view(retrieve=extend_schema( - summary='Method returns the information about the function', tags=['lambda'], versions=['1.0'])) + summary='Method returns the information about the function', tags=['lambda'], versions=['2.0'])) @extend_schema_view(list=extend_schema( - summary='Method returns a list of functions', tags=['lambda'], versions=['1.0'])) + summary='Method returns a list of functions', tags=['lambda'], versions=['2.0'])) class FunctionViewSet(viewsets.ViewSet): lookup_value_regex = '[a-zA-Z0-9_.-]+' lookup_field = 'func_id' @@ -590,14 +590,14 @@ def call(self, request, func_id): return lambda_func.invoke(db_task, request.data) @extend_schema_view(retrieve=extend_schema( - summary='Method returns the status of the request', tags=['lambda'], versions=['1.0'])) + summary='Method returns the status of the request', tags=['lambda'], versions=['2.0'])) @extend_schema_view(list=extend_schema( - summary='Method returns a list of requests', tags=['lambda'], versions=['1.0'])) + summary='Method returns a list of requests', tags=['lambda'], versions=['2.0'])) #TODO @extend_schema_view(create=extend_schema( - summary='Method calls the function', tags=['lambda'], versions=['1.0'])) + summary='Method calls the function', tags=['lambda'], versions=['2.0'])) @extend_schema_view(delete=extend_schema( - summary='Method cancels the request', tags=['lambda'], versions=['1.0'])) + summary='Method cancels the request', tags=['lambda'], versions=['2.0'])) class RequestViewSet(viewsets.ViewSet): iam_organization_field = None diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index bc01326538e..73f4e82a2e9 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -23,32 +23,32 @@ summary='Method returns details of an organization', responses={ '200': OrganizationReadSerializer, - }, tags=['organizations'], versions=['1.0'])) + }, tags=['organizations'], versions=['2.0'])) @extend_schema_view(list=extend_schema( summary='Method returns a paginated list of organizatins according to query parameters', responses={ '200': OrganizationReadSerializer(many=True), - }, tags=['organizations'], versions=['1.0'])) + }, tags=['organizations'], versions=['2.0'])) @extend_schema_view(update=extend_schema( summary='Method updates an organization by id', responses={ '200': OrganizationWriteSerializer, - }, tags=['organizations'], versions=['1.0'])) + }, tags=['organizations'], versions=['2.0'])) @extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in an organization', responses={ '200': OrganizationWriteSerializer, - }, tags=['organizations'], versions=['1.0'])) + }, tags=['organizations'], versions=['2.0'])) @extend_schema_view(create=extend_schema( summary='Method creates an organization', responses={ '201': OrganizationWriteSerializer, - }, tags=['organizations'], versions=['1.0'])) + }, tags=['organizations'], versions=['2.0'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes an organization', responses={ '204': OpenApiResponse('The organization has been deleted'), - }, tags=['organizations'], versions=['1.0'])) + }, tags=['organizations'], versions=['2.0'])) class OrganizationViewSet(viewsets.ModelViewSet): queryset = Organization.objects.all() ordering = ['-id'] @@ -83,27 +83,27 @@ class Meta: summary='Method returns details of a membership', responses={ '200': MembershipReadSerializer, - }, tags=['memberships'], versions=['1.0'])) + }, tags=['memberships'], versions=['2.0'])) @extend_schema_view(list=extend_schema( summary='Method returns a paginated list of memberships according to query parameters', responses={ '200': MembershipReadSerializer(many=True), - }, tags=['memberships'], versions=['1.0'])) + }, tags=['memberships'], versions=['2.0'])) @extend_schema_view(update=extend_schema( summary='Method updates a membership by id', responses={ '200': MembershipWriteSerializer, - }, tags=['memberships'], versions=['1.0'])) + }, tags=['memberships'], versions=['2.0'])) @extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in a membership', responses={ '200': MembershipWriteSerializer, - }, tags=['memberships'], versions=['1.0'])) + }, tags=['memberships'], versions=['2.0'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes a membership', responses={ '204': OpenApiResponse('The membership has been deleted'), - }, tags=['memberships'], versions=['1.0'])) + }, tags=['memberships'], versions=['2.0'])) class MembershipViewSet(mixins.RetrieveModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet): queryset = Membership.objects.all() @@ -128,32 +128,32 @@ def get_queryset(self): summary='Method returns details of an invitation', responses={ '200': InvitationReadSerializer, - }, tags=['invitations'], versions=['1.0'])) + }, tags=['invitations'], versions=['2.0'])) @extend_schema_view(list=extend_schema( summary='Method returns a paginated list of invitations according to query parameters', responses={ '200': InvitationReadSerializer(many=True), - }, tags=['invitations'], versions=['1.0'])) + }, tags=['invitations'], versions=['2.0'])) @extend_schema_view(update=extend_schema( summary='Method updates an invitation by id', responses={ '200': InvitationWriteSerializer, - }, tags=['invitations'], versions=['1.0'])) + }, tags=['invitations'], versions=['2.0'])) @extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in an invitation', responses={ '200': InvitationWriteSerializer, - }, tags=['invitations'], versions=['1.0'])) + }, tags=['invitations'], versions=['2.0'])) @extend_schema_view(create=extend_schema( summary='Method creates an invitation', responses={ '201': InvitationWriteSerializer, - }, tags=['invitations'], versions=['1.0'])) + }, tags=['invitations'], versions=['2.0'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes an invitation', responses={ '204': OpenApiResponse('The invitation has been deleted'), - }, tags=['invitations'], versions=['1.0'])) + }, tags=['invitations'], versions=['2.0'])) class InvitationViewSet(viewsets.ModelViewSet): queryset = Invitation.objects.all() ordering = ['-created_date'] diff --git a/cvat/apps/restrictions/views.py b/cvat/apps/restrictions/views.py index 9863d45725a..9b239f8c123 100644 --- a/cvat/apps/restrictions/views.py +++ b/cvat/apps/restrictions/views.py @@ -27,7 +27,7 @@ def get_serializer(self, *args, **kwargs): @staticmethod @extend_schema(summary='Method provides user agreements that the user must accept to register', responses={'200': UserAgreementSerializer}, - tags=['restrictions'], versions=['1.0']) + tags=['restrictions'], versions=['2.0']) @action(detail=False, methods=['GET'], serializer_class=UserAgreementSerializer, url_path='user-agreements') def user_agreements(request): user_agreements = settings.RESTRICTIONS['user_agreements'] @@ -38,7 +38,7 @@ def user_agreements(request): @staticmethod @extend_schema(summary='Method provides CVAT terms of use', responses={'200': OpenApiResponse(description='CVAT terms of use')}, - tags=['restrictions'], versions=['1.0']) + tags=['restrictions'], versions=['2.0']) @action(detail=False, methods=['GET'], renderer_classes=(TemplateHTMLRenderer,), url_path='terms-of-use') def terms_of_use(request): From 9a1da1de3a449a9215e0cc61605ea6b47b68f45d Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 4 Feb 2022 21:48:57 +0300 Subject: [PATCH 15/22] Fix missed org/org_id parameters in the swagger doc --- cvat/apps/iam/__init__.py | 1 + cvat/apps/iam/filters.py | 14 ++++++++++---- cvat/apps/iam/schema.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 cvat/apps/iam/schema.py diff --git a/cvat/apps/iam/__init__.py b/cvat/apps/iam/__init__.py index 813d16ca0ec..6aa30772efb 100644 --- a/cvat/apps/iam/__init__.py +++ b/cvat/apps/iam/__init__.py @@ -2,3 +2,4 @@ # # SPDX-License-Identifier: MIT +from .schema import OrganizationFilterExtension \ No newline at end of file diff --git a/cvat/apps/iam/filters.py b/cvat/apps/iam/filters.py index 3a0539d5830..9b7d483efa5 100644 --- a/cvat/apps/iam/filters.py +++ b/cvat/apps/iam/filters.py @@ -6,12 +6,18 @@ from rest_framework.filters import BaseFilterBackend class OrganizationFilterBackend(BaseFilterBackend): + organization_slug = 'org' + organization_slug_description = 'Organization unique slug' + organization_id = 'org_id' + organization_id_description = 'Organization identifier' + def get_schema_fields(self, view): return [ - coreapi.Field(name='org', location='query', required=False, - type='string', description='Organization unique slug'), - coreapi.Field(name='org_id', location='query', required=False, - type='string', description='Organization identifier'), + # NOTE: in coreapi.Field 'type', 'description' and 'example' are now deprecated, in favor of 'schema'. + coreapi.Field(name=self.organization_slug, location='query', required=False, + type='string', description=self.organization_slug_description), + coreapi.Field(name=self.organization_id, location='query', required=False, + type='string', description=self.organization_id_description), ] def filter_queryset(self, request, queryset, view): diff --git a/cvat/apps/iam/schema.py b/cvat/apps/iam/schema.py new file mode 100644 index 00000000000..09ac68d016a --- /dev/null +++ b/cvat/apps/iam/schema.py @@ -0,0 +1,33 @@ +# Copyright (C) 2022 Intel Corporation +# +# SPDX-License-Identifier: MIT + +from drf_spectacular.extensions import OpenApiFilterExtension +from drf_spectacular.plumbing import build_parameter_type +from drf_spectacular.utils import OpenApiParameter + +# https://drf-spectacular.readthedocs.io/en/latest/customization.html?highlight=OpenApiFilterExtension#step-5-extensions +class OrganizationFilterExtension(OpenApiFilterExtension): + """Describe OrganizationFilterBackend filter""" + + target_class = 'cvat.apps.iam.filters.OrganizationFilterBackend' + priority = 1 + + def get_schema_operation_parameters(self, auto_schema, *args, **kwargs): + """Describe query parameters""" + return [ + build_parameter_type( + name=self.target.organization_slug, + required=False, + location=OpenApiParameter.QUERY, + description=self.target.organization_slug_description, + schema={'type': 'string'}, + ), + build_parameter_type( + name=self.target.organization_id, + required=False, + location=OpenApiParameter.QUERY, + description=self.target.organization_id_description, + schema={'type': 'string'}, + ) + ] \ No newline at end of file From c784f572fcf36f6a842071f4324097f0c1492613 Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 4 Feb 2022 22:22:15 +0300 Subject: [PATCH 16/22] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bedf1080e67..b6cc6fde422 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Different resources (tasks, projects) are not visible anymore for all CVAT instance users by default () - API versioning scheme: using accept header versioning instead of namespace versioning () - Replaced 'django_sendfile' with 'django_sendfile2' () +- Use drf-spectacular instead of drf-yasg for swagger documentation () ### Deprecated - Job field "status" is not used in UI anymore, but it has not been removed from the database yet () From 7ad61e5fca8e6f0b4d0149cfc052af7a606cd2b7 Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 4 Feb 2022 22:30:55 +0300 Subject: [PATCH 17/22] Add item to to-do list --- cvat/apps/engine/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 9c935caaab1..b6dc15533df 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -1318,6 +1318,8 @@ def self(self, request): serializer = serializer_class(request.user, context={ "request": request }) return Response(serializer.data) +# TODO: it will be good to find a way to define description using drf_spectacular. +# But now it will be enough to use an example # class RedefineDescriptionField(FieldInspector): # # pylint: disable=no-self-use # def process_result(self, result, method_name, obj, **kwargs): From dfc8c24094df8ac15fb5ce47f79117cd08a2fd23 Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 8 Feb 2022 10:19:01 +0300 Subject: [PATCH 18/22] Revert settings --- cvat/settings/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 686d4d948c0..517be013e57 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -168,6 +168,8 @@ def add_ssh_keys(): 'rest_framework.filters.OrderingFilter', 'cvat.apps.iam.filters.OrganizationFilterBackend'), + # Disable default handling of the 'format' query parameter by REST framework + 'URL_FORMAT_OVERRIDE': 'scheme', 'DEFAULT_THROTTLE_CLASSES': [ 'rest_framework.throttling.AnonRateThrottle', ], From 4d6a493e0019608686a2ada70fe9b92b7df1416b Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 11 Feb 2022 09:41:17 +0300 Subject: [PATCH 19/22] Some fixes after validation --- cvat/apps/engine/views.py | 54 +++++++++++++++---------------- cvat/apps/iam/__init__.py | 2 +- cvat/apps/iam/schema.py | 15 +++++++-- cvat/apps/iam/views.py | 3 +- cvat/apps/lambda_manager/views.py | 19 +++++++++-- cvat/apps/organizations/urls.py | 1 - cvat/apps/organizations/views.py | 6 ++-- cvat/apps/training/views.py | 1 + 8 files changed, 63 insertions(+), 38 deletions(-) diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index b6dc15533df..c7dff4dbd5e 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -41,7 +41,7 @@ import cvat.apps.dataset_manager as dm import cvat.apps.dataset_manager.views # pylint: disable=unused-import -from cvat.apps.engine.cloud_provider import get_cloud_storage_instance, Credentials, Status +from cvat.apps.engine.cloud_provider import get_cloud_storage_instance, Credentials, Status as CloudStorageStatus from cvat.apps.dataset_manager.bindings import CvatImportError from cvat.apps.dataset_manager.serializers import DatasetFormatsSerializer from cvat.apps.engine.frame_provider import FrameProvider @@ -254,7 +254,7 @@ class Meta: '200': PolymorphicProxySerializer(component_name='PolymorphicProject', serializers=[ ProjectSerializer, ProjectSearchSerializer, - ], resource_type_field_name='type', many=True), + ], resource_type_field_name='name', many=True), }, tags=['projects'], versions=['2.0'])) @extend_schema_view(create=extend_schema( summary='Method creates a new project', @@ -454,7 +454,7 @@ def export_backup(self, request, pk=None): @extend_schema(summary='Methods create a project from a backup', responses={ - '201': OpenApiResponse(description='The project has been imported'), + '201': OpenApiResponse(description='The project has been imported'), # or better specify {id: project_id} '202': OpenApiResponse(description='Importing a backup file has been started'), }, tags=['projects'], versions=['2.0']) @action(detail=False, methods=['POST'], url_path='backup') @@ -589,7 +589,7 @@ class Meta: @extend_schema_view(create=extend_schema( summary='Method creates a new task in a database without any attached images and videos', responses={ - '201': OpenApiResponse(description='The task has been created'), + '201': TaskSerializer, }, tags=['tasks'], versions=['2.0'])) @extend_schema_view(retrieve=extend_schema( summary='Method returns details of a specific task', @@ -602,7 +602,7 @@ class Meta: @extend_schema_view(destroy=extend_schema( summary='Method deletes a specific task, all attached jobs, annotations, and data', responses={ - '204': OpenApiResponse('The task has been deleted'), + '204': OpenApiResponse(description='The task has been deleted'), }, tags=['tasks'], versions=['2.0'])) @extend_schema_view(partial_update=extend_schema( summary='Methods does a partial update of chosen fields in a task', @@ -631,7 +631,7 @@ def get_queryset(self): @extend_schema(summary='Method recreates a task from an attached task backup file', responses={ - '201': OpenApiResponse(description='The task has been imported'), + '201': OpenApiResponse(description='The task has been imported'), # or better specify {id: task_id} '202': OpenApiResponse(description='Importing a backup file has been started'), }, tags=['tasks'], versions=['2.0']) @action(detail=False, methods=['POST'], url_path='backup') @@ -735,7 +735,7 @@ def upload_finished(self, request): description='Finishes data upload. Can be combined with Upload-Start header to create task data with one request'), ], responses={ - '202': OpenApiResponse(), + '202': OpenApiResponse(description=''), }, tags=['tasks'], versions=['2.0']) @extend_schema(methods=['GET'], summary='Method returns data for a specific task', parameters=[ @@ -749,7 +749,7 @@ def upload_finished(self, request): description="A unique number value identifying chunk or frame, doesn't matter for 'preview' type"), ], responses={ - '200': OpenApiResponse('Data of a specific type'), + '200': OpenApiResponse(description='Data of a specific type'), }, tags=['tasks'], versions=['2.0']) @action(detail=True, methods=['OPTIONS', 'POST', 'GET'], url_path=r'data/?$') def data(self, request, pk): @@ -1029,11 +1029,11 @@ def get_serializer_class(self): ], responses={ #TODO - '200': OpenApiResponse(), + '200': OpenApiResponse(description=''), }, tags=['jobs'], versions=['2.0']) @extend_schema(methods=['DELETE'], summary='Method deletes all annotations for a specific job', responses={ - '204': OpenApiResponse('The annotation has been deleted'), + '204': OpenApiResponse(description='The annotation has been deleted'), }, tags=['jobs'], versions=['2.0']) @action(detail=True, methods=['GET', 'DELETE', 'PUT', 'PATCH'], serializer_class=LabeledDataSerializer) @@ -1145,7 +1145,7 @@ def data(self, request, pk): @extend_schema_view(destroy=extend_schema( summary='Method deletes an issue', responses={ - '204': OpenApiResponse('The issue has been deleted'), + '204': OpenApiResponse(description='The issue has been deleted'), }, tags=['issues'], versions=['2.0'])) class IssueViewSet(viewsets.ModelViewSet): queryset = Issue.objects.all().order_by('-id') @@ -1210,7 +1210,7 @@ def comments(self, request, pk): @extend_schema_view(destroy=extend_schema( summary='Method deletes a comment', responses={ - '204': OpenApiResponse('The comment has been deleted'), + '204': OpenApiResponse(description='The comment has been deleted'), }, tags=['comments'], versions=['2.0'])) class CommentViewSet(viewsets.ModelViewSet): queryset = Comment.objects.all().order_by('-id') @@ -1251,7 +1251,7 @@ class Meta: '200': PolymorphicProxySerializer(component_name='MetaUser', serializers=[ UserSerializer, BasicUserSerializer, - ], resource_type_field_name='type'), + ], resource_type_field_name='username'), }, tags=['users'], versions=['2.0'])) @extend_schema_view(retrieve=extend_schema( summary='Method provides information of a specific user', @@ -1259,7 +1259,7 @@ class Meta: '200': PolymorphicProxySerializer(component_name='MetaUser', serializers=[ UserSerializer, BasicUserSerializer, - ], resource_type_field_name='type'), + ], resource_type_field_name='username'), }, tags=['users'], versions=['2.0'])) @extend_schema_view(partial_update=extend_schema( summary='Method updates chosen fields of a user', @@ -1267,12 +1267,12 @@ class Meta: '200': PolymorphicProxySerializer(component_name='MetaUser', serializers=[ UserSerializer, BasicUserSerializer, - ], resource_type_field_name='type'), + ], resource_type_field_name='username'), }, tags=['users'], versions=['2.0'])) @extend_schema_view(destroy=extend_schema( summary='Method deletes a specific user from the server', responses={ - '204': OpenApiResponse('The user has been deleted'), + '204': OpenApiResponse(description='The user has been deleted'), }, tags=['users'], versions=['2.0'])) class UserViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin): @@ -1307,7 +1307,7 @@ def get_serializer_class(self): '200': PolymorphicProxySerializer(component_name='MetaUser', serializers=[ UserSerializer, BasicUserSerializer, - ], resource_type_field_name='type'), + ], resource_type_field_name='username'), }, tags=['users'], versions=['2.0']) @action(detail=False, methods=['GET']) def self(self, request): @@ -1444,7 +1444,7 @@ def create(self, request, *args, **kwargs): location=OpenApiParameter.QUERY, type=OpenApiTypes.STR), ], responses={ - '200': OpenApiResponse(description='A manifest content'), + '200': OpenApiResponse(response=OpenApiTypes.OBJECT, description='A manifest content'), }, tags=['cloud storages'], versions=['2.0']) @action(detail=True, methods=['GET'], url_path='content') def content(self, request, pk): @@ -1466,10 +1466,10 @@ def content(self, request, pk): raise Exception('There is no manifest file') manifest_path = request.query_params.get('manifest_path', db_storage.manifests.first().filename) file_status = storage.get_file_status(manifest_path) - if file_status == Status.NOT_FOUND: + if file_status == CloudStorageStatus.NOT_FOUND: raise FileNotFoundError(errno.ENOENT, "Not found on the cloud storage {}".format(db_storage.display_name), manifest_path) - elif file_status == Status.FORBIDDEN: + elif file_status == CloudStorageStatus.FORBIDDEN: raise PermissionError(errno.EACCES, "Access to the file on the '{}' cloud storage is denied".format(db_storage.display_name), manifest_path) @@ -1494,9 +1494,9 @@ def content(self, request, pk): except Exception as ex: # check that cloud storage was not deleted storage_status = storage.get_status() if storage else None - if storage_status == Status.FORBIDDEN: + if storage_status == CloudStorageStatus.FORBIDDEN: msg = 'The resource {} is no longer available. Access forbidden.'.format(storage.name) - elif storage_status == Status.NOT_FOUND: + elif storage_status == CloudStorageStatus.NOT_FOUND: msg = 'The resource {} not found. It may have been deleted.'.format(storage.name) else: msg = str(ex) @@ -1548,10 +1548,10 @@ def preview(self, request, pk): return HttpResponseBadRequest(msg) file_status = storage.get_file_status(preview_path) - if file_status == Status.NOT_FOUND: + if file_status == CloudStorageStatus.NOT_FOUND: raise FileNotFoundError(errno.ENOENT, "Not found on the cloud storage {}".format(db_storage.display_name), preview_path) - elif file_status == Status.FORBIDDEN: + elif file_status == CloudStorageStatus.FORBIDDEN: raise PermissionError(errno.EACCES, "Access to the file on the '{}' cloud storage is denied".format(db_storage.display_name), preview_path) with NamedTemporaryFile() as temp_image: @@ -1570,9 +1570,9 @@ def preview(self, request, pk): except Exception as ex: # check that cloud storage was not deleted storage_status = storage.get_status() if storage else None - if storage_status == Status.FORBIDDEN: + if storage_status == CloudStorageStatus.FORBIDDEN: msg = 'The resource {} is no longer available. Access forbidden.'.format(storage.name) - elif storage_status == Status.NOT_FOUND: + elif storage_status == CloudStorageStatus.NOT_FOUND: msg = 'The resource {} not found. It may have been deleted.'.format(storage.name) else: msg = str(ex) @@ -1580,7 +1580,7 @@ def preview(self, request, pk): @extend_schema(summary='Method returns a cloud storage status', responses={ - '200': OpenApiResponse(description='Cloud Storage status'), + '200': OpenApiResponse(response=OpenApiTypes.STR, description='Cloud Storage status (AVAILABLE | NOT_FOUND | FORBIDDEN)'), }, tags=['cloud storages'], versions=['2.0']) @action(detail=True, methods=['GET'], url_path='status') def status(self, request, pk): diff --git a/cvat/apps/iam/__init__.py b/cvat/apps/iam/__init__.py index 6aa30772efb..23dfb721d6d 100644 --- a/cvat/apps/iam/__init__.py +++ b/cvat/apps/iam/__init__.py @@ -2,4 +2,4 @@ # # SPDX-License-Identifier: MIT -from .schema import OrganizationFilterExtension \ No newline at end of file +from .schema import * \ No newline at end of file diff --git a/cvat/apps/iam/schema.py b/cvat/apps/iam/schema.py index 09ac68d016a..6cb7a3213f4 100644 --- a/cvat/apps/iam/schema.py +++ b/cvat/apps/iam/schema.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: MIT -from drf_spectacular.extensions import OpenApiFilterExtension +from drf_spectacular.extensions import OpenApiFilterExtension, OpenApiAuthenticationExtension from drf_spectacular.plumbing import build_parameter_type from drf_spectacular.utils import OpenApiParameter @@ -30,4 +30,15 @@ def get_schema_operation_parameters(self, auto_schema, *args, **kwargs): description=self.target.organization_id_description, schema={'type': 'string'}, ) - ] \ No newline at end of file + ] + +class SignatureAuthenticationScheme(OpenApiAuthenticationExtension): + target_class = 'cvat.apps.iam.authentication.SignatureAuthentication' + name = 'SignatureAuthentication' # name used in the schema + + def get_security_definition(self, auto_schema): + return { + 'type': 'apiKey', + 'in': 'query', + 'name': 'sign', + } \ No newline at end of file diff --git a/cvat/apps/iam/views.py b/cvat/apps/iam/views.py index 20a8b1ca494..9ebff150d1d 100644 --- a/cvat/apps/iam/views.py +++ b/cvat/apps/iam/views.py @@ -12,6 +12,7 @@ from allauth.account import app_settings as allauth_settings from furl import furl +from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer, extend_schema_view @@ -88,7 +89,7 @@ def __call__(self, request): 'url': serializers.CharField(), } ), - responses={'200': OpenApiResponse(description='text URL')}, tags=['auth'], versions=['2.0'])) + responses={'200': OpenApiResponse(response=OpenApiTypes.STR, description='text URL')}, tags=['auth'], versions=['2.0'])) class SigningView(views.APIView): def post(self, request): diff --git a/cvat/apps/lambda_manager/views.py b/cvat/apps/lambda_manager/views.py index b9312f1ef79..18cc4db6064 100644 --- a/cvat/apps/lambda_manager/views.py +++ b/cvat/apps/lambda_manager/views.py @@ -17,7 +17,8 @@ from cvat.apps.engine.serializers import LabeledDataSerializer from cvat.apps.engine.models import ShapeType, SourceType -from drf_spectacular.utils import extend_schema, extend_schema_view +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse, OpenApiParameter +from drf_spectacular.types import OpenApiTypes class LambdaType(Enum): DETECTOR = "detector" @@ -553,13 +554,18 @@ def func_wrapper(*args, **kwargs): return wrap_response @extend_schema_view(retrieve=extend_schema( - summary='Method returns the information about the function', tags=['lambda'], versions=['2.0'])) + summary='Method returns the information about the function', + responses={ + '200': OpenApiResponse(response=OpenApiTypes.OBJECT, description='Information about the function'), + }, + tags=['lambda'], versions=['2.0'])) @extend_schema_view(list=extend_schema( summary='Method returns a list of functions', tags=['lambda'], versions=['2.0'])) class FunctionViewSet(viewsets.ViewSet): lookup_value_regex = '[a-zA-Z0-9_.-]+' lookup_field = 'func_id' iam_organization_field = None + serializer_class = None @return_response() def list(self, request): @@ -590,7 +596,13 @@ def call(self, request, func_id): return lambda_func.invoke(db_task, request.data) @extend_schema_view(retrieve=extend_schema( - summary='Method returns the status of the request', tags=['lambda'], versions=['2.0'])) + summary='Method returns the status of the request', + parameters=[ + # specify correct type + OpenApiParameter('id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, + description='Request id'), + ], + tags=['lambda'], versions=['2.0'])) @extend_schema_view(list=extend_schema( summary='Method returns a list of requests', tags=['lambda'], versions=['2.0'])) #TODO @@ -600,6 +612,7 @@ def call(self, request, func_id): summary='Method cancels the request', tags=['lambda'], versions=['2.0'])) class RequestViewSet(viewsets.ViewSet): iam_organization_field = None + serializer_class = None @return_response() def list(self, request): diff --git a/cvat/apps/organizations/urls.py b/cvat/apps/organizations/urls.py index 79438727e24..fa05f0f76dd 100644 --- a/cvat/apps/organizations/urls.py +++ b/cvat/apps/organizations/urls.py @@ -2,7 +2,6 @@ # # SPDX-License-Identifier: MIT -#from django.urls import include, path from rest_framework.routers import DefaultRouter from .views import InvitationViewSet, MembershipViewSet, OrganizationViewSet diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index 73f4e82a2e9..d6def621cb4 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -47,7 +47,7 @@ @extend_schema_view(destroy=extend_schema( summary='Method deletes an organization', responses={ - '204': OpenApiResponse('The organization has been deleted'), + '204': OpenApiResponse(description='The organization has been deleted'), }, tags=['organizations'], versions=['2.0'])) class OrganizationViewSet(viewsets.ModelViewSet): queryset = Organization.objects.all() @@ -102,7 +102,7 @@ class Meta: @extend_schema_view(destroy=extend_schema( summary='Method deletes a membership', responses={ - '204': OpenApiResponse('The membership has been deleted'), + '204': OpenApiResponse(description='The membership has been deleted'), }, tags=['memberships'], versions=['2.0'])) class MembershipViewSet(mixins.RetrieveModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet): @@ -152,7 +152,7 @@ def get_queryset(self): @extend_schema_view(destroy=extend_schema( summary='Method deletes an invitation', responses={ - '204': OpenApiResponse('The invitation has been deleted'), + '204': OpenApiResponse(description='The invitation has been deleted'), }, tags=['invitations'], versions=['2.0'])) class InvitationViewSet(viewsets.ModelViewSet): queryset = Invitation.objects.all() diff --git a/cvat/apps/training/views.py b/cvat/apps/training/views.py index 93277bbdedb..da98cd970f5 100644 --- a/cvat/apps/training/views.py +++ b/cvat/apps/training/views.py @@ -8,6 +8,7 @@ class PredictView(viewsets.ViewSet): + serializer_class = None def get_permissions(self): permissions = [IsAuthenticated] From 6a140176f65207ed488a52bff0c5efab37848014 Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 11 Feb 2022 09:53:13 +0300 Subject: [PATCH 20/22] Update drf spectacular version --- cvat/requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/requirements/base.txt b/cvat/requirements/base.txt index 32e093a15b6..44f940991e4 100644 --- a/cvat/requirements/base.txt +++ b/cvat/requirements/base.txt @@ -28,7 +28,7 @@ django-filter==2.4.0 Markdown==3.2.2 djangorestframework==3.12.4 Pygments==2.7.4 -drf-spectacular==0.21.1 +drf-spectacular==0.21.2 Shapely==1.7.1 pdf2image==1.14.0 django-rest-auth[with_social]==0.9.5 From eb3e546f2972b7f461f9c116fc472a185c12f65a Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 11 Feb 2022 18:34:19 +0300 Subject: [PATCH 21/22] Remove servers settings block --- cvat/settings/base.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 517be013e57..a8becd802cc 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -518,29 +518,6 @@ def add_ssh_keys(): 'SCHEMA_PATH_PREFIX': '/api', 'SCHEMA_PATH_PREFIX_TRIM': True, 'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'], - 'SERVERS': [ - # https://swagger.io/specification/#schema-object - { - 'url': '{protocol}://localhost:7000/api/', - 'description': 'Development server', - 'variables': { - 'protocol': { - 'enum': ['http', 'https'], - 'default': 'http', - } - }, - }, - { - 'url': '{protocol}://localhost:8080/api/', - 'description': 'Production server', - 'variables': { - 'protocol': { - 'enum': ['http', 'https'], - 'default': 'http', - } - }, - } - ], # https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/ 'SWAGGER_UI_SETTINGS': { 'deepLinking': True, From 0070e22d636db8ccc06755e2030da6c987918d3b Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 11 Feb 2022 19:34:04 +0300 Subject: [PATCH 22/22] Fix after removing SERVERS settings --- cvat/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/settings/base.py b/cvat/settings/base.py index a8becd802cc..1f104746316 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -516,7 +516,7 @@ def add_ssh_keys(): 'SERVE_PUBLIC': True, 'SCHEMA_COERCE_PATH_PK_SUFFIX': True, 'SCHEMA_PATH_PREFIX': '/api', - 'SCHEMA_PATH_PREFIX_TRIM': True, + 'SCHEMA_PATH_PREFIX_TRIM': False, 'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'], # https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/ 'SWAGGER_UI_SETTINGS': {