@ -40,7 +40,7 @@ from cvat.apps.engine.serializers import (TaskSerializer, UserSerializer,
RqStatusSerializer , TaskDataSerializer , LabeledDataSerializer ,
PluginSerializer , FileInfoSerializer , LogEventSerializer ,
ProjectSerializer , BasicUserSerializer )
from cvat . apps . annotation . serializers import AnnotationFileSerializer
from cvat . apps . annotation . serializers import AnnotationFileSerializer , AnnotationFormatSerializer
from django . contrib . auth . models import User
from django . core . exceptions import ObjectDoesNotExist
from cvat . apps . authentication import auth
@ -49,6 +49,12 @@ from cvat.apps.annotation.models import AnnotationDumper, AnnotationLoader
from cvat . apps . annotation . format import get_annotation_formats
import cvat . apps . dataset_manager . task as DatumaroTask
from drf_yasg . utils import swagger_auto_schema
from drf_yasg import openapi
from django . utils . decorators import method_decorator
from drf_yasg . inspectors import NotHandled , CoreAPICompatInspector
from django_filters . rest_framework import DjangoFilterBackend
# Server REST API
@login_required
def dispatch_request ( request ) :
@ -79,6 +85,8 @@ class ServerViewSet(viewsets.ViewSet):
pass
@staticmethod
@swagger_auto_schema ( method = ' get ' , operation_summary = ' Method provides basic CVAT information ' ,
responses = { ' 200 ' : AboutSerializer } )
@action ( detail = False , methods = [ ' GET ' ] , serializer_class = AboutSerializer )
def about ( request ) :
from cvat import __version__ as cvat_version
@ -98,8 +106,14 @@ class ServerViewSet(viewsets.ViewSet):
return Response ( data = serializer . data )
@staticmethod
@swagger_auto_schema ( method = ' post ' , request_body = ExceptionSerializer )
@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 = {
@ -119,8 +133,14 @@ class ServerViewSet(viewsets.ViewSet):
return Response ( serializer . data , status = status . HTTP_201_CREATED )
@staticmethod
@swagger_auto_schema ( method = ' post ' , request_body = LogEventSerializer ( many = True ) )
@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 }
@ -137,6 +157,11 @@ class ServerViewSet(viewsets.ViewSet):
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 ) }
)
@action ( detail = False , methods = [ ' GET ' ] , serializer_class = FileInfoSerializer )
def share ( request ) :
param = request . query_params . get ( ' directory ' , ' / ' )
@ -165,6 +190,8 @@ class ServerViewSet(viewsets.ViewSet):
status = status . HTTP_400_BAD_REQUEST )
@staticmethod
@swagger_auto_schema ( method = ' get ' , operation_summary = ' Method provides the list of available annotations formats supported by the server ' ,
responses = { ' 200 ' : AnnotationFormatSerializer ( many = True ) } )
@action ( detail = False , methods = [ ' GET ' ] , url_path = ' annotation/formats ' )
def annotation_formats ( request ) :
data = get_annotation_formats ( )
@ -187,6 +214,23 @@ class ProjectFilter(filters.FilterSet):
model = models . Project
fields = ( " id " , " name " , " owner " , " status " , " assignee " )
@method_decorator ( name = ' list ' , decorator = swagger_auto_schema (
operation_summary = ' Returns a paginated list of projects according to query parameters (10 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 ( ' assignee ' , openapi . IN_QUERY , description = " Find all projects where assignee name contains a parameter value " ,
type = openapi . TYPE_STRING ) ] ) )
@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 ' ) )
class ProjectViewSet ( auth . ProjectGetQuerySetMixin , viewsets . ModelViewSet ) :
queryset = models . Project . objects . all ( ) . order_by ( ' -id ' )
serializer_class = ProjectSerializer
@ -218,6 +262,8 @@ class ProjectViewSet(auth.ProjectGetQuerySetMixin, viewsets.ModelViewSet):
else :
serializer . save ( owner = self . request . user )
@swagger_auto_schema ( method = ' get ' , operation_summary = ' Returns information of the tasks of the project with the selected id ' ,
responses = { ' 200 ' : TaskSerializer ( many = True ) } )
@action ( detail = True , methods = [ ' GET ' ] , serializer_class = TaskSerializer )
def tasks ( self , request , pk ) :
self . get_object ( ) # force to call check_object_permissions
@ -247,6 +293,35 @@ class TaskFilter(filters.FilterSet):
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 )
] ,
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 ' ) )
class TaskViewSet ( auth . TaskGetQuerySetMixin , viewsets . ModelViewSet ) :
queryset = Task . objects . all ( ) . prefetch_related (
" label_set__attributespec_set " ,
@ -285,6 +360,8 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
super ( ) . perform_destroy ( instance )
shutil . rmtree ( task_dirname , ignore_errors = True )
@swagger_auto_schema ( method = ' get ' , operation_summary = ' Returns a list of jobs for a specific task ' ,
responses = { ' 200 ' : JobSerializer ( many = True ) } )
@action ( detail = True , methods = [ ' GET ' ] , serializer_class = JobSerializer )
def jobs ( self , request , pk ) :
self . get_object ( ) # force to call check_object_permissions
@ -294,8 +371,12 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
return Response ( serializer . data )
@swagger_auto_schema ( method = ' post ' , operation_summary = ' Method permanently attaches images or video to a task ' )
@action ( detail = True , methods = [ ' POST ' ] , serializer_class = TaskDataSerializer )
def data ( self , request , pk ) :
"""
These data cannot be changed later
"""
db_task = self . get_object ( ) # call check_object_permissions as well
serializer = TaskDataSerializer ( db_task , data = request . data )
if serializer . is_valid ( raise_exception = True ) :
@ -303,6 +384,12 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
task . create ( db_task . id , serializer . data )
return Response ( serializer . data , status = status . HTTP_202_ACCEPTED )
@swagger_auto_schema ( method = ' get ' , operation_summary = ' Method returns annotations for a specific task ' )
@swagger_auto_schema ( method = ' put ' , operation_summary = ' Method performs an update of all annotations in a specific task ' )
@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 ) :
@ -341,9 +428,23 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
return Response ( data = str ( e ) , status = status . HTTP_400_BAD_REQUEST )
return Response ( data )
@swagger_auto_schema ( method = ' get ' , operation_summary = ' Method allows to download annotations as a file ' ,
manual_parameters = [ openapi . Parameter ( ' filename ' , openapi . IN_PATH , description = " A name of a file with annotations " ,
type = openapi . TYPE_STRING , required = True ) ,
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 " ,
type = openapi . TYPE_STRING , required = True ) ,
openapi . Parameter ( ' action ' , in_ = openapi . IN_QUERY , description = ' Used to start downloading process after annotation file had been created ' ,
required = False , enum = [ ' download ' ] , type = openapi . TYPE_STRING ) ] ,
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 ' ) } )
@action ( detail = True , methods = [ ' GET ' ] , serializer_class = None ,
url_path = ' annotations/(?P<filename>[^/]+) ' )
def dump ( self , request , pk , filename ) :
"""
Dump of annotations in common case is a long process which cannot be performed within one request .
First request starts dumping process . When the file is ready ( code 201 ) you can get it with query parameter action = download .
"""
filename = re . sub ( r ' [ \\ /*?: " <>|] ' , ' _ ' , filename )
username = request . user . username
db_task = self . get_object ( ) # call check_object_permissions as well
@ -402,6 +503,7 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
return Response ( status = status . HTTP_202_ACCEPTED )
@swagger_auto_schema ( method = ' get ' , operation_summary = ' When task is being created the method returns information about a status of the creation process ' )
@action ( detail = True , methods = [ ' GET ' ] , serializer_class = RqStatusSerializer )
def status ( self , request , pk ) :
self . get_object ( ) # force to call check_object_permissions
@ -430,6 +532,8 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
return response
@swagger_auto_schema ( method = ' get ' , operation_summary = ' Method provides a list of sizes (width, height) of media files which are related with the task ' ,
responses = { ' 200 ' : ImageMetaSerializer ( many = True ) } )
@action ( detail = True , methods = [ ' GET ' ] , serializer_class = ImageMetaSerializer ,
url_path = ' frames/meta ' )
def data_info ( self , request , pk ) :
@ -445,11 +549,13 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
if serializer . is_valid ( raise_exception = True ) :
return Response ( serializer . data )
@swagger_auto_schema ( method = ' get ' , manual_parameters = [ openapi . Parameter ( ' frame ' , openapi . IN_PATH , required = True ,
description = " A unique integer value identifying this frame " , type = openapi . TYPE_INTEGER ) ] ,
operation_summary = ' Method returns a specific frame for a specific task ' ,
responses = { ' 200 ' : openapi . Response ( description = ' frame ' ) } )
@action ( detail = True , methods = [ ' GET ' ] , serializer_class = None ,
url_path = ' frames/(?P<frame> \ d+) ' )
def frame ( self , request , pk , frame ) :
""" Get a frame for the task """
try :
# Follow symbol links if the frame is a link on a real image otherwise
# mimetype detection inside sendfile will work incorrectly.
@ -461,10 +567,16 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
" cannot get frame # {} " . format ( frame ) , exc_info = True )
return HttpResponseBadRequest ( str ( e ) )
@swagger_auto_schema ( method = ' get ' , operation_summary = ' Export task as a dataset in a specific format ' ,
manual_parameters = [ openapi . Parameter ( ' action ' , in_ = openapi . IN_QUERY ,
required = False , type = openapi . TYPE_STRING , enum = [ ' download ' ] ) ,
openapi . Parameter ( ' format ' , in_ = openapi . IN_QUERY , required = False , type = openapi . TYPE_STRING ) ] ,
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 ' ) } )
@action ( detail = True , methods = [ ' GET ' ] , serializer_class = None ,
url_path = ' dataset ' )
def dataset_export ( self , request , pk ) :
""" Export task as a dataset in a specific format """
db_task = self . get_object ( )
@ -528,6 +640,10 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
result_ttl = ttl , failure_ttl = ttl )
return Response ( status = status . HTTP_202_ACCEPTED )
@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 ' ) )
class JobViewSet ( viewsets . GenericViewSet ,
mixins . RetrieveModelMixin , mixins . UpdateModelMixin ) :
queryset = Job . objects . all ( ) . order_by ( ' id ' )
@ -546,7 +662,13 @@ class JobViewSet(viewsets.GenericViewSet,
return [ perm ( ) for perm in permissions ]
@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 )
def annotations ( self , request , pk ) :
@ -587,6 +709,14 @@ class JobViewSet(viewsets.GenericViewSet,
return Response ( data = str ( e ) , status = status . HTTP_400_BAD_REQUEST )
return Response ( data )
@method_decorator ( name = ' list ' , decorator = swagger_auto_schema (
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 ' ) )
class UserViewSet ( viewsets . GenericViewSet , mixins . ListModelMixin ,
mixins . RetrieveModelMixin , mixins . UpdateModelMixin , mixins . DestroyModelMixin ) :
queryset = User . objects . all ( ) . order_by ( ' id ' )
@ -615,8 +745,12 @@ class UserViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
return [ perm ( ) for perm in permissions ]
@swagger_auto_schema ( method = ' get ' , operation_summary = ' Method returns an instance of a user who is currently authorized ' )
@action ( detail = False , methods = [ ' GET ' ] )
def self ( self , request ) :
"""
Method returns an instance of a user who is currently authorized
"""
serializer_class = self . get_serializer_class ( )
serializer = serializer_class ( request . user , context = { " request " : request } )
return Response ( serializer . data )
@ -657,6 +791,15 @@ 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 load_data_proxy ( request , rq_id , rq_func , pk ) :
queue = django_rq . get_queue ( " default " )
rq_job = queue . fetch_job ( rq_id )