Skip to content

Commit b45702e

Browse files
Marishka17Chris Lee-Messer
authored and
Chris Lee-Messer
committedMar 5, 2020
Added documentation for swagger page (cvat-ai#936)
1 parent 5b3c5ef commit b45702e

File tree

2 files changed

+170
-5
lines changed

2 files changed

+170
-5
lines changed
 

‎cvat/apps/authentication/views.py

+22
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
from . import forms
1414
from . import signature
1515

16+
from django.utils.decorators import method_decorator
17+
from drf_yasg.utils import swagger_auto_schema
18+
from drf_yasg import openapi
19+
1620
def register_user(request):
1721
if request.method == 'POST':
1822
form = forms.NewUserForm(request.POST)
@@ -27,7 +31,25 @@ def register_user(request):
2731
form = forms.NewUserForm()
2832
return render(request, 'register.html', {'form': form})
2933

34+
@method_decorator(name='post', decorator=swagger_auto_schema(
35+
request_body=openapi.Schema(
36+
type=openapi.TYPE_OBJECT,
37+
required=[
38+
'url'
39+
],
40+
properties={
41+
'url': openapi.Schema(type=openapi.TYPE_STRING)
42+
}
43+
),
44+
responses={'200': openapi.Response(description='text URL')}
45+
))
3046
class SigningView(views.APIView):
47+
"""
48+
This method signs URL for access to the server.
49+
50+
Signed URL contains a token which authenticates a user on the server.
51+
Signed URL is valid during 30 seconds since signing.
52+
"""
3153
def post(self, request):
3254
url = request.data.get('url')
3355
if not url:

‎cvat/apps/engine/views.py

+148-5
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
RqStatusSerializer, TaskDataSerializer, LabeledDataSerializer,
4141
PluginSerializer, FileInfoSerializer, LogEventSerializer,
4242
ProjectSerializer, BasicUserSerializer)
43-
from cvat.apps.annotation.serializers import AnnotationFileSerializer
43+
from cvat.apps.annotation.serializers import AnnotationFileSerializer, AnnotationFormatSerializer
4444
from django.contrib.auth.models import User
4545
from django.core.exceptions import ObjectDoesNotExist
4646
from cvat.apps.authentication import auth
@@ -49,6 +49,12 @@
4949
from cvat.apps.annotation.format import get_annotation_formats
5050
import cvat.apps.dataset_manager.task as DatumaroTask
5151

52+
from drf_yasg.utils import swagger_auto_schema
53+
from drf_yasg import openapi
54+
from django.utils.decorators import method_decorator
55+
from drf_yasg.inspectors import NotHandled, CoreAPICompatInspector
56+
from django_filters.rest_framework import DjangoFilterBackend
57+
5258
# Server REST API
5359
@login_required
5460
def dispatch_request(request):
@@ -79,6 +85,8 @@ def get_serializer(self, *args, **kwargs):
7985
pass
8086

8187
@staticmethod
88+
@swagger_auto_schema(method='get', operation_summary='Method provides basic CVAT information',
89+
responses={'200': AboutSerializer})
8290
@action(detail=False, methods=['GET'], serializer_class=AboutSerializer)
8391
def about(request):
8492
from cvat import __version__ as cvat_version
@@ -98,8 +106,14 @@ def about(request):
98106
return Response(data=serializer.data)
99107

100108
@staticmethod
109+
@swagger_auto_schema(method='post', request_body=ExceptionSerializer)
101110
@action(detail=False, methods=['POST'], serializer_class=ExceptionSerializer)
102111
def exception(request):
112+
"""
113+
Saves an exception from a client on the server
114+
115+
Sends logs to the ELK if it is connected
116+
"""
103117
serializer = ExceptionSerializer(data=request.data)
104118
if serializer.is_valid(raise_exception=True):
105119
additional_info = {
@@ -119,8 +133,14 @@ def exception(request):
119133
return Response(serializer.data, status=status.HTTP_201_CREATED)
120134

121135
@staticmethod
136+
@swagger_auto_schema(method='post', request_body=LogEventSerializer(many=True))
122137
@action(detail=False, methods=['POST'], serializer_class=LogEventSerializer)
123138
def logs(request):
139+
"""
140+
Saves logs from a client on the server
141+
142+
Sends logs to the ELK if it is connected
143+
"""
124144
serializer = LogEventSerializer(many=True, data=request.data)
125145
if serializer.is_valid(raise_exception=True):
126146
user = { "username": request.user.username }
@@ -137,6 +157,11 @@ def logs(request):
137157
return Response(serializer.data, status=status.HTTP_201_CREATED)
138158

139159
@staticmethod
160+
@swagger_auto_schema(
161+
method='get', operation_summary='Returns all files and folders that are on the server along specified path',
162+
manual_parameters=[openapi.Parameter('directory', openapi.IN_QUERY, type=openapi.TYPE_STRING, description='Directory to browse')],
163+
responses={'200' : FileInfoSerializer(many=True)}
164+
)
140165
@action(detail=False, methods=['GET'], serializer_class=FileInfoSerializer)
141166
def share(request):
142167
param = request.query_params.get('directory', '/')
@@ -165,6 +190,8 @@ def share(request):
165190
status=status.HTTP_400_BAD_REQUEST)
166191

167192
@staticmethod
193+
@swagger_auto_schema(method='get', operation_summary='Method provides the list of available annotations formats supported by the server',
194+
responses={'200': AnnotationFormatSerializer(many=True)})
168195
@action(detail=False, methods=['GET'], url_path='annotation/formats')
169196
def annotation_formats(request):
170197
data = get_annotation_formats()
@@ -187,6 +214,23 @@ class Meta:
187214
model = models.Project
188215
fields = ("id", "name", "owner", "status", "assignee")
189216

217+
@method_decorator(name='list', decorator=swagger_auto_schema(
218+
operation_summary='Returns a paginated list of projects according to query parameters (10 projects per page)',
219+
manual_parameters=[
220+
openapi.Parameter('id', openapi.IN_QUERY, description="A unique number value identifying this project",
221+
type=openapi.TYPE_NUMBER),
222+
openapi.Parameter('name', openapi.IN_QUERY, description="Find all projects where name contains a parameter value",
223+
type=openapi.TYPE_STRING),
224+
openapi.Parameter('owner', openapi.IN_QUERY, description="Find all project where owner name contains a parameter value",
225+
type=openapi.TYPE_STRING),
226+
openapi.Parameter('status', openapi.IN_QUERY, description="Find all projects with a specific status",
227+
type=openapi.TYPE_STRING, enum=[str(i) for i in StatusChoice]),
228+
openapi.Parameter('assignee', openapi.IN_QUERY, description="Find all projects where assignee name contains a parameter value",
229+
type=openapi.TYPE_STRING)]))
230+
@method_decorator(name='create', decorator=swagger_auto_schema(operation_summary='Method creates a new project'))
231+
@method_decorator(name='retrieve', decorator=swagger_auto_schema(operation_summary='Method returns details of a specific project'))
232+
@method_decorator(name='destroy', decorator=swagger_auto_schema(operation_summary='Method deletes a specific project'))
233+
@method_decorator(name='partial_update', decorator=swagger_auto_schema(operation_summary='Methods does a partial update of chosen fields in a project'))
190234
class ProjectViewSet(auth.ProjectGetQuerySetMixin, viewsets.ModelViewSet):
191235
queryset = models.Project.objects.all().order_by('-id')
192236
serializer_class = ProjectSerializer
@@ -218,6 +262,8 @@ def perform_create(self, serializer):
218262
else:
219263
serializer.save(owner=self.request.user)
220264

265+
@swagger_auto_schema(method='get', operation_summary='Returns information of the tasks of the project with the selected id',
266+
responses={'200': TaskSerializer(many=True)})
221267
@action(detail=True, methods=['GET'], serializer_class=TaskSerializer)
222268
def tasks(self, request, pk):
223269
self.get_object() # force to call check_object_permissions
@@ -247,6 +293,35 @@ class Meta:
247293
fields = ("id", "project_id", "project", "name", "owner", "mode", "status",
248294
"assignee")
249295

296+
class DjangoFilterInspector(CoreAPICompatInspector):
297+
def get_filter_parameters(self, filter_backend):
298+
if isinstance(filter_backend, DjangoFilterBackend):
299+
result = super(DjangoFilterInspector, self).get_filter_parameters(filter_backend)
300+
res = result.copy()
301+
302+
for param in result:
303+
if param.get('name') == 'project_id' or param.get('name') == 'project':
304+
res.remove(param)
305+
return res
306+
307+
return NotHandled
308+
309+
@method_decorator(name='list', decorator=swagger_auto_schema(
310+
operation_summary='Returns a paginated list of tasks according to query parameters (10 tasks per page)',
311+
manual_parameters=[
312+
openapi.Parameter('id',openapi.IN_QUERY,description="A unique number value identifying this task",type=openapi.TYPE_NUMBER),
313+
openapi.Parameter('name', openapi.IN_QUERY, description="Find all tasks where name contains a parameter value", type=openapi.TYPE_STRING),
314+
openapi.Parameter('owner', openapi.IN_QUERY, description="Find all tasks where owner name contains a parameter value", type=openapi.TYPE_STRING),
315+
openapi.Parameter('mode', openapi.IN_QUERY, description="Find all tasks with a specific mode", type=openapi.TYPE_STRING, enum=['annotation', 'interpolation']),
316+
openapi.Parameter('status', openapi.IN_QUERY, description="Find all tasks with a specific status", type=openapi.TYPE_STRING,enum=['annotation','validation','completed']),
317+
openapi.Parameter('assignee', openapi.IN_QUERY, description="Find all tasks where assignee name contains a parameter value", type=openapi.TYPE_STRING)
318+
],
319+
filter_inspectors=[DjangoFilterInspector]))
320+
@method_decorator(name='create', decorator=swagger_auto_schema(operation_summary='Method creates a new task in a database without any attached images and videos'))
321+
@method_decorator(name='retrieve', decorator=swagger_auto_schema(operation_summary='Method returns details of a specific task'))
322+
@method_decorator(name='update', decorator=swagger_auto_schema(operation_summary='Method updates a task by id'))
323+
@method_decorator(name='destroy', decorator=swagger_auto_schema(operation_summary='Method deletes a specific task, all attached jobs, annotations, and data'))
324+
@method_decorator(name='partial_update', decorator=swagger_auto_schema(operation_summary='Methods does a partial update of chosen fields in a task'))
250325
class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
251326
queryset = Task.objects.all().prefetch_related(
252327
"label_set__attributespec_set",
@@ -285,6 +360,8 @@ def perform_destroy(self, instance):
285360
super().perform_destroy(instance)
286361
shutil.rmtree(task_dirname, ignore_errors=True)
287362

363+
@swagger_auto_schema(method='get', operation_summary='Returns a list of jobs for a specific task',
364+
responses={'200': JobSerializer(many=True)})
288365
@action(detail=True, methods=['GET'], serializer_class=JobSerializer)
289366
def jobs(self, request, pk):
290367
self.get_object() # force to call check_object_permissions
@@ -294,15 +371,25 @@ def jobs(self, request, pk):
294371

295372
return Response(serializer.data)
296373

374+
@swagger_auto_schema(method='post', operation_summary='Method permanently attaches images or video to a task')
297375
@action(detail=True, methods=['POST'], serializer_class=TaskDataSerializer)
298376
def data(self, request, pk):
377+
"""
378+
These data cannot be changed later
379+
"""
299380
db_task = self.get_object() # call check_object_permissions as well
300381
serializer = TaskDataSerializer(db_task, data=request.data)
301382
if serializer.is_valid(raise_exception=True):
302383
serializer.save()
303384
task.create(db_task.id, serializer.data)
304385
return Response(serializer.data, status=status.HTTP_202_ACCEPTED)
305386

387+
@swagger_auto_schema(method='get', operation_summary='Method returns annotations for a specific task')
388+
@swagger_auto_schema(method='put', operation_summary='Method performs an update of all annotations in a specific task')
389+
@swagger_auto_schema(method='patch', operation_summary='Method performs a partial update of annotations in a specific task',
390+
manual_parameters=[openapi.Parameter('action', in_=openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING,
391+
enum=['create', 'update', 'delete'])])
392+
@swagger_auto_schema(method='delete', operation_summary='Method deletes all annotations for a specific task')
306393
@action(detail=True, methods=['GET', 'DELETE', 'PUT', 'PATCH'],
307394
serializer_class=LabeledDataSerializer)
308395
def annotations(self, request, pk):
@@ -341,9 +428,23 @@ def annotations(self, request, pk):
341428
return Response(data=str(e), status=status.HTTP_400_BAD_REQUEST)
342429
return Response(data)
343430

431+
@swagger_auto_schema(method='get', operation_summary='Method allows to download annotations as a file',
432+
manual_parameters=[openapi.Parameter('filename', openapi.IN_PATH, description="A name of a file with annotations",
433+
type=openapi.TYPE_STRING, required=True),
434+
openapi.Parameter('format', openapi.IN_QUERY, description="A name of a dumper\nYou can get annotation dumpers from this API:\n/server/annotation/formats",
435+
type=openapi.TYPE_STRING, required=True),
436+
openapi.Parameter('action', in_=openapi.IN_QUERY, description='Used to start downloading process after annotation file had been created',
437+
required=False, enum=['download'], type=openapi.TYPE_STRING)],
438+
responses={'202': openapi.Response(description='Dump of annotations has been started'),
439+
'201': openapi.Response(description='Annotations file is ready to download'),
440+
'200': openapi.Response(description='Download of file started')})
344441
@action(detail=True, methods=['GET'], serializer_class=None,
345442
url_path='annotations/(?P<filename>[^/]+)')
346443
def dump(self, request, pk, filename):
444+
"""
445+
Dump of annotations in common case is a long process which cannot be performed within one request.
446+
First request starts dumping process. When the file is ready (code 201) you can get it with query parameter action=download.
447+
"""
347448
filename = re.sub(r'[\\/*?:"<>|]', '_', filename)
348449
username = request.user.username
349450
db_task = self.get_object() # call check_object_permissions as well
@@ -402,6 +503,7 @@ def dump(self, request, pk, filename):
402503

403504
return Response(status=status.HTTP_202_ACCEPTED)
404505

506+
@swagger_auto_schema(method='get', operation_summary='When task is being created the method returns information about a status of the creation process')
405507
@action(detail=True, methods=['GET'], serializer_class=RqStatusSerializer)
406508
def status(self, request, pk):
407509
self.get_object() # force to call check_object_permissions
@@ -430,6 +532,8 @@ def _get_rq_response(queue, job_id):
430532

431533
return response
432534

535+
@swagger_auto_schema(method='get', operation_summary='Method provides a list of sizes (width, height) of media files which are related with the task',
536+
responses={'200': ImageMetaSerializer(many=True)})
433537
@action(detail=True, methods=['GET'], serializer_class=ImageMetaSerializer,
434538
url_path='frames/meta')
435539
def data_info(self, request, pk):
@@ -445,11 +549,13 @@ def data_info(self, request, pk):
445549
if serializer.is_valid(raise_exception=True):
446550
return Response(serializer.data)
447551

552+
@swagger_auto_schema(method='get', manual_parameters=[openapi.Parameter('frame', openapi.IN_PATH, required=True,
553+
description="A unique integer value identifying this frame", type=openapi.TYPE_INTEGER)],
554+
operation_summary='Method returns a specific frame for a specific task',
555+
responses={'200': openapi.Response(description='frame')})
448556
@action(detail=True, methods=['GET'], serializer_class=None,
449557
url_path='frames/(?P<frame>\d+)')
450558
def frame(self, request, pk, frame):
451-
"""Get a frame for the task"""
452-
453559
try:
454560
# Follow symbol links if the frame is a link on a real image otherwise
455561
# mimetype detection inside sendfile will work incorrectly.
@@ -461,10 +567,16 @@ def frame(self, request, pk, frame):
461567
"cannot get frame #{}".format(frame), exc_info=True)
462568
return HttpResponseBadRequest(str(e))
463569

570+
@swagger_auto_schema(method='get', operation_summary='Export task as a dataset in a specific format',
571+
manual_parameters=[openapi.Parameter('action', in_=openapi.IN_QUERY,
572+
required=False, type=openapi.TYPE_STRING, enum=['download']),
573+
openapi.Parameter('format', in_=openapi.IN_QUERY, required=False, type=openapi.TYPE_STRING)],
574+
responses={'202': openapi.Response(description='Dump of annotations has been started'),
575+
'201': openapi.Response(description='Annotations file is ready to download'),
576+
'200': openapi.Response(description='Download of file started')})
464577
@action(detail=True, methods=['GET'], serializer_class=None,
465578
url_path='dataset')
466579
def dataset_export(self, request, pk):
467-
"""Export task as a dataset in a specific format"""
468580

469581
db_task = self.get_object()
470582

@@ -528,6 +640,10 @@ def dataset_export(self, request, pk):
528640
result_ttl=ttl, failure_ttl=ttl)
529641
return Response(status=status.HTTP_202_ACCEPTED)
530642

643+
@method_decorator(name='retrieve', decorator=swagger_auto_schema(operation_summary='Method returns details of a job'))
644+
@method_decorator(name='update', decorator=swagger_auto_schema(operation_summary='Method updates a job by id'))
645+
@method_decorator(name='partial_update', decorator=swagger_auto_schema(
646+
operation_summary='Methods does a partial update of chosen fields in a job'))
531647
class JobViewSet(viewsets.GenericViewSet,
532648
mixins.RetrieveModelMixin, mixins.UpdateModelMixin):
533649
queryset = Job.objects.all().order_by('id')
@@ -546,7 +662,13 @@ def get_permissions(self):
546662

547663
return [perm() for perm in permissions]
548664

549-
665+
@swagger_auto_schema(method='get', operation_summary='Method returns annotations for a specific job')
666+
@swagger_auto_schema(method='put', operation_summary='Method performs an update of all annotations in a specific job')
667+
@swagger_auto_schema(method='patch', manual_parameters=[
668+
openapi.Parameter('action', in_=openapi.IN_QUERY, type=openapi.TYPE_STRING, required=True,
669+
enum=['create', 'update', 'delete'])],
670+
operation_summary='Method performs a partial update of annotations in a specific job')
671+
@swagger_auto_schema(method='delete', operation_summary='Method deletes all annotations for a specific job')
550672
@action(detail=True, methods=['GET', 'DELETE', 'PUT', 'PATCH'],
551673
serializer_class=LabeledDataSerializer)
552674
def annotations(self, request, pk):
@@ -587,6 +709,14 @@ def annotations(self, request, pk):
587709
return Response(data=str(e), status=status.HTTP_400_BAD_REQUEST)
588710
return Response(data)
589711

712+
@method_decorator(name='list', decorator=swagger_auto_schema(
713+
operation_summary='Method provides a paginated list of users registered on the server'))
714+
@method_decorator(name='retrieve', decorator=swagger_auto_schema(
715+
operation_summary='Method provides information of a specific user'))
716+
@method_decorator(name='partial_update', decorator=swagger_auto_schema(
717+
operation_summary='Method updates chosen fields of a user'))
718+
@method_decorator(name='destroy', decorator=swagger_auto_schema(
719+
operation_summary='Method deletes a specific user from the server'))
590720
class UserViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
591721
mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin):
592722
queryset = User.objects.all().order_by('id')
@@ -615,8 +745,12 @@ def get_permissions(self):
615745

616746
return [perm() for perm in permissions]
617747

748+
@swagger_auto_schema(method='get', operation_summary='Method returns an instance of a user who is currently authorized')
618749
@action(detail=False, methods=['GET'])
619750
def self(self, request):
751+
"""
752+
Method returns an instance of a user who is currently authorized
753+
"""
620754
serializer_class = self.get_serializer_class()
621755
serializer = serializer_class(request.user, context={ "request": request })
622756
return Response(serializer.data)
@@ -657,6 +791,15 @@ def rq_handler(job, exc_type, exc_value, tb):
657791

658792
return True
659793

794+
# TODO: Method should be reimplemented as a separated view
795+
# @swagger_auto_schema(method='put', manual_parameters=[openapi.Parameter('format', in_=openapi.IN_QUERY,
796+
# description='A name of a loader\nYou can get annotation loaders from this API:\n/server/annotation/formats',
797+
# required=True, type=openapi.TYPE_STRING)],
798+
# operation_summary='Method allows to upload annotations',
799+
# responses={'202': openapi.Response(description='Load of annotations has been started'),
800+
# '201': openapi.Response(description='Annotations have been uploaded')},
801+
# tags=['tasks'])
802+
# @api_view(['PUT'])
660803
def load_data_proxy(request, rq_id, rq_func, pk):
661804
queue = django_rq.get_queue("default")
662805
rq_job = queue.fetch_job(rq_id)

0 commit comments

Comments
 (0)
Please sign in to comment.