40
40
RqStatusSerializer , TaskDataSerializer , LabeledDataSerializer ,
41
41
PluginSerializer , FileInfoSerializer , LogEventSerializer ,
42
42
ProjectSerializer , BasicUserSerializer )
43
- from cvat .apps .annotation .serializers import AnnotationFileSerializer
43
+ from cvat .apps .annotation .serializers import AnnotationFileSerializer , AnnotationFormatSerializer
44
44
from django .contrib .auth .models import User
45
45
from django .core .exceptions import ObjectDoesNotExist
46
46
from cvat .apps .authentication import auth
49
49
from cvat .apps .annotation .format import get_annotation_formats
50
50
import cvat .apps .dataset_manager .task as DatumaroTask
51
51
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
+
52
58
# Server REST API
53
59
@login_required
54
60
def dispatch_request (request ):
@@ -79,6 +85,8 @@ def get_serializer(self, *args, **kwargs):
79
85
pass
80
86
81
87
@staticmethod
88
+ @swagger_auto_schema (method = 'get' , operation_summary = 'Method provides basic CVAT information' ,
89
+ responses = {'200' : AboutSerializer })
82
90
@action (detail = False , methods = ['GET' ], serializer_class = AboutSerializer )
83
91
def about (request ):
84
92
from cvat import __version__ as cvat_version
@@ -98,8 +106,14 @@ def about(request):
98
106
return Response (data = serializer .data )
99
107
100
108
@staticmethod
109
+ @swagger_auto_schema (method = 'post' , request_body = ExceptionSerializer )
101
110
@action (detail = False , methods = ['POST' ], serializer_class = ExceptionSerializer )
102
111
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
+ """
103
117
serializer = ExceptionSerializer (data = request .data )
104
118
if serializer .is_valid (raise_exception = True ):
105
119
additional_info = {
@@ -119,8 +133,14 @@ def exception(request):
119
133
return Response (serializer .data , status = status .HTTP_201_CREATED )
120
134
121
135
@staticmethod
136
+ @swagger_auto_schema (method = 'post' , request_body = LogEventSerializer (many = True ))
122
137
@action (detail = False , methods = ['POST' ], serializer_class = LogEventSerializer )
123
138
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
+ """
124
144
serializer = LogEventSerializer (many = True , data = request .data )
125
145
if serializer .is_valid (raise_exception = True ):
126
146
user = { "username" : request .user .username }
@@ -137,6 +157,11 @@ def logs(request):
137
157
return Response (serializer .data , status = status .HTTP_201_CREATED )
138
158
139
159
@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
+ )
140
165
@action (detail = False , methods = ['GET' ], serializer_class = FileInfoSerializer )
141
166
def share (request ):
142
167
param = request .query_params .get ('directory' , '/' )
@@ -165,6 +190,8 @@ def share(request):
165
190
status = status .HTTP_400_BAD_REQUEST )
166
191
167
192
@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 )})
168
195
@action (detail = False , methods = ['GET' ], url_path = 'annotation/formats' )
169
196
def annotation_formats (request ):
170
197
data = get_annotation_formats ()
@@ -187,6 +214,23 @@ class Meta:
187
214
model = models .Project
188
215
fields = ("id" , "name" , "owner" , "status" , "assignee" )
189
216
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' ))
190
234
class ProjectViewSet (auth .ProjectGetQuerySetMixin , viewsets .ModelViewSet ):
191
235
queryset = models .Project .objects .all ().order_by ('-id' )
192
236
serializer_class = ProjectSerializer
@@ -218,6 +262,8 @@ def perform_create(self, serializer):
218
262
else :
219
263
serializer .save (owner = self .request .user )
220
264
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 )})
221
267
@action (detail = True , methods = ['GET' ], serializer_class = TaskSerializer )
222
268
def tasks (self , request , pk ):
223
269
self .get_object () # force to call check_object_permissions
@@ -247,6 +293,35 @@ class Meta:
247
293
fields = ("id" , "project_id" , "project" , "name" , "owner" , "mode" , "status" ,
248
294
"assignee" )
249
295
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' ))
250
325
class TaskViewSet (auth .TaskGetQuerySetMixin , viewsets .ModelViewSet ):
251
326
queryset = Task .objects .all ().prefetch_related (
252
327
"label_set__attributespec_set" ,
@@ -285,6 +360,8 @@ def perform_destroy(self, instance):
285
360
super ().perform_destroy (instance )
286
361
shutil .rmtree (task_dirname , ignore_errors = True )
287
362
363
+ @swagger_auto_schema (method = 'get' , operation_summary = 'Returns a list of jobs for a specific task' ,
364
+ responses = {'200' : JobSerializer (many = True )})
288
365
@action (detail = True , methods = ['GET' ], serializer_class = JobSerializer )
289
366
def jobs (self , request , pk ):
290
367
self .get_object () # force to call check_object_permissions
@@ -294,15 +371,25 @@ def jobs(self, request, pk):
294
371
295
372
return Response (serializer .data )
296
373
374
+ @swagger_auto_schema (method = 'post' , operation_summary = 'Method permanently attaches images or video to a task' )
297
375
@action (detail = True , methods = ['POST' ], serializer_class = TaskDataSerializer )
298
376
def data (self , request , pk ):
377
+ """
378
+ These data cannot be changed later
379
+ """
299
380
db_task = self .get_object () # call check_object_permissions as well
300
381
serializer = TaskDataSerializer (db_task , data = request .data )
301
382
if serializer .is_valid (raise_exception = True ):
302
383
serializer .save ()
303
384
task .create (db_task .id , serializer .data )
304
385
return Response (serializer .data , status = status .HTTP_202_ACCEPTED )
305
386
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' )
306
393
@action (detail = True , methods = ['GET' , 'DELETE' , 'PUT' , 'PATCH' ],
307
394
serializer_class = LabeledDataSerializer )
308
395
def annotations (self , request , pk ):
@@ -341,9 +428,23 @@ def annotations(self, request, pk):
341
428
return Response (data = str (e ), status = status .HTTP_400_BAD_REQUEST )
342
429
return Response (data )
343
430
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\n You 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' )})
344
441
@action (detail = True , methods = ['GET' ], serializer_class = None ,
345
442
url_path = 'annotations/(?P<filename>[^/]+)' )
346
443
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
+ """
347
448
filename = re .sub (r'[\\/*?:"<>|]' , '_' , filename )
348
449
username = request .user .username
349
450
db_task = self .get_object () # call check_object_permissions as well
@@ -402,6 +503,7 @@ def dump(self, request, pk, filename):
402
503
403
504
return Response (status = status .HTTP_202_ACCEPTED )
404
505
506
+ @swagger_auto_schema (method = 'get' , operation_summary = 'When task is being created the method returns information about a status of the creation process' )
405
507
@action (detail = True , methods = ['GET' ], serializer_class = RqStatusSerializer )
406
508
def status (self , request , pk ):
407
509
self .get_object () # force to call check_object_permissions
@@ -430,6 +532,8 @@ def _get_rq_response(queue, job_id):
430
532
431
533
return response
432
534
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 )})
433
537
@action (detail = True , methods = ['GET' ], serializer_class = ImageMetaSerializer ,
434
538
url_path = 'frames/meta' )
435
539
def data_info (self , request , pk ):
@@ -445,11 +549,13 @@ def data_info(self, request, pk):
445
549
if serializer .is_valid (raise_exception = True ):
446
550
return Response (serializer .data )
447
551
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' )})
448
556
@action (detail = True , methods = ['GET' ], serializer_class = None ,
449
557
url_path = 'frames/(?P<frame>\d+)' )
450
558
def frame (self , request , pk , frame ):
451
- """Get a frame for the task"""
452
-
453
559
try :
454
560
# Follow symbol links if the frame is a link on a real image otherwise
455
561
# mimetype detection inside sendfile will work incorrectly.
@@ -461,10 +567,16 @@ def frame(self, request, pk, frame):
461
567
"cannot get frame #{}" .format (frame ), exc_info = True )
462
568
return HttpResponseBadRequest (str (e ))
463
569
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' )})
464
577
@action (detail = True , methods = ['GET' ], serializer_class = None ,
465
578
url_path = 'dataset' )
466
579
def dataset_export (self , request , pk ):
467
- """Export task as a dataset in a specific format"""
468
580
469
581
db_task = self .get_object ()
470
582
@@ -528,6 +640,10 @@ def dataset_export(self, request, pk):
528
640
result_ttl = ttl , failure_ttl = ttl )
529
641
return Response (status = status .HTTP_202_ACCEPTED )
530
642
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' ))
531
647
class JobViewSet (viewsets .GenericViewSet ,
532
648
mixins .RetrieveModelMixin , mixins .UpdateModelMixin ):
533
649
queryset = Job .objects .all ().order_by ('id' )
@@ -546,7 +662,13 @@ def get_permissions(self):
546
662
547
663
return [perm () for perm in permissions ]
548
664
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' )
550
672
@action (detail = True , methods = ['GET' , 'DELETE' , 'PUT' , 'PATCH' ],
551
673
serializer_class = LabeledDataSerializer )
552
674
def annotations (self , request , pk ):
@@ -587,6 +709,14 @@ def annotations(self, request, pk):
587
709
return Response (data = str (e ), status = status .HTTP_400_BAD_REQUEST )
588
710
return Response (data )
589
711
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' ))
590
720
class UserViewSet (viewsets .GenericViewSet , mixins .ListModelMixin ,
591
721
mixins .RetrieveModelMixin , mixins .UpdateModelMixin , mixins .DestroyModelMixin ):
592
722
queryset = User .objects .all ().order_by ('id' )
@@ -615,8 +745,12 @@ def get_permissions(self):
615
745
616
746
return [perm () for perm in permissions ]
617
747
748
+ @swagger_auto_schema (method = 'get' , operation_summary = 'Method returns an instance of a user who is currently authorized' )
618
749
@action (detail = False , methods = ['GET' ])
619
750
def self (self , request ):
751
+ """
752
+ Method returns an instance of a user who is currently authorized
753
+ """
620
754
serializer_class = self .get_serializer_class ()
621
755
serializer = serializer_class (request .user , context = { "request" : request })
622
756
return Response (serializer .data )
@@ -657,6 +791,15 @@ def rq_handler(job, exc_type, exc_value, tb):
657
791
658
792
return True
659
793
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'])
660
803
def load_data_proxy (request , rq_id , rq_func , pk ):
661
804
queue = django_rq .get_queue ("default" )
662
805
rq_job = queue .fetch_job (rq_id )
0 commit comments