Added documentation for swagger page (#936)

main
Maya 6 years ago committed by Nikita Manovich
parent 728a51c8e6
commit 4587afe09a

@ -13,6 +13,10 @@ from furl import furl
from . import forms from . import forms
from . import signature from . import signature
from django.utils.decorators import method_decorator
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
def register_user(request): def register_user(request):
if request.method == 'POST': if request.method == 'POST':
form = forms.NewUserForm(request.POST) form = forms.NewUserForm(request.POST)
@ -27,7 +31,25 @@ def register_user(request):
form = forms.NewUserForm() form = forms.NewUserForm()
return render(request, 'register.html', {'form': form}) return render(request, 'register.html', {'form': form})
@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): 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): def post(self, request):
url = request.data.get('url') url = request.data.get('url')
if not url: if not url:

@ -40,7 +40,7 @@ from cvat.apps.engine.serializers import (TaskSerializer, UserSerializer,
RqStatusSerializer, TaskDataSerializer, LabeledDataSerializer, RqStatusSerializer, TaskDataSerializer, LabeledDataSerializer,
PluginSerializer, FileInfoSerializer, LogEventSerializer, PluginSerializer, FileInfoSerializer, LogEventSerializer,
ProjectSerializer, BasicUserSerializer) 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.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from cvat.apps.authentication import auth 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 from cvat.apps.annotation.format import get_annotation_formats
import cvat.apps.dataset_manager.task as DatumaroTask 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 # Server REST API
@login_required @login_required
def dispatch_request(request): def dispatch_request(request):
@ -79,6 +85,8 @@ class ServerViewSet(viewsets.ViewSet):
pass pass
@staticmethod @staticmethod
@swagger_auto_schema(method='get', operation_summary='Method provides basic CVAT information',
responses={'200': AboutSerializer})
@action(detail=False, methods=['GET'], serializer_class=AboutSerializer) @action(detail=False, methods=['GET'], serializer_class=AboutSerializer)
def about(request): def about(request):
from cvat import __version__ as cvat_version from cvat import __version__ as cvat_version
@ -98,8 +106,14 @@ class ServerViewSet(viewsets.ViewSet):
return Response(data=serializer.data) return Response(data=serializer.data)
@staticmethod @staticmethod
@swagger_auto_schema(method='post', request_body=ExceptionSerializer)
@action(detail=False, methods=['POST'], serializer_class=ExceptionSerializer) @action(detail=False, methods=['POST'], serializer_class=ExceptionSerializer)
def exception(request): 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) serializer = ExceptionSerializer(data=request.data)
if serializer.is_valid(raise_exception=True): if serializer.is_valid(raise_exception=True):
additional_info = { additional_info = {
@ -119,8 +133,14 @@ class ServerViewSet(viewsets.ViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
@staticmethod @staticmethod
@swagger_auto_schema(method='post', request_body=LogEventSerializer(many=True))
@action(detail=False, methods=['POST'], serializer_class=LogEventSerializer) @action(detail=False, methods=['POST'], serializer_class=LogEventSerializer)
def logs(request): 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) serializer = LogEventSerializer(many=True, data=request.data)
if serializer.is_valid(raise_exception=True): if serializer.is_valid(raise_exception=True):
user = { "username": request.user.username } user = { "username": request.user.username }
@ -137,6 +157,11 @@ class ServerViewSet(viewsets.ViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
@staticmethod @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) @action(detail=False, methods=['GET'], serializer_class=FileInfoSerializer)
def share(request): def share(request):
param = request.query_params.get('directory', '/') param = request.query_params.get('directory', '/')
@ -165,6 +190,8 @@ class ServerViewSet(viewsets.ViewSet):
status=status.HTTP_400_BAD_REQUEST) status=status.HTTP_400_BAD_REQUEST)
@staticmethod @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') @action(detail=False, methods=['GET'], url_path='annotation/formats')
def annotation_formats(request): def annotation_formats(request):
data = get_annotation_formats() data = get_annotation_formats()
@ -187,6 +214,23 @@ class ProjectFilter(filters.FilterSet):
model = models.Project model = models.Project
fields = ("id", "name", "owner", "status", "assignee") 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): class ProjectViewSet(auth.ProjectGetQuerySetMixin, viewsets.ModelViewSet):
queryset = models.Project.objects.all().order_by('-id') queryset = models.Project.objects.all().order_by('-id')
serializer_class = ProjectSerializer serializer_class = ProjectSerializer
@ -218,6 +262,8 @@ class ProjectViewSet(auth.ProjectGetQuerySetMixin, viewsets.ModelViewSet):
else: else:
serializer.save(owner=self.request.user) 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) @action(detail=True, methods=['GET'], serializer_class=TaskSerializer)
def tasks(self, request, pk): def tasks(self, request, pk):
self.get_object() # force to call check_object_permissions 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", fields = ("id", "project_id", "project", "name", "owner", "mode", "status",
"assignee") "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): class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
queryset = Task.objects.all().prefetch_related( queryset = Task.objects.all().prefetch_related(
"label_set__attributespec_set", "label_set__attributespec_set",
@ -285,6 +360,8 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
super().perform_destroy(instance) super().perform_destroy(instance)
shutil.rmtree(task_dirname, ignore_errors=True) 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) @action(detail=True, methods=['GET'], serializer_class=JobSerializer)
def jobs(self, request, pk): def jobs(self, request, pk):
self.get_object() # force to call check_object_permissions self.get_object() # force to call check_object_permissions
@ -294,8 +371,12 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
return Response(serializer.data) 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) @action(detail=True, methods=['POST'], serializer_class=TaskDataSerializer)
def data(self, request, pk): def data(self, request, pk):
"""
These data cannot be changed later
"""
db_task = self.get_object() # call check_object_permissions as well db_task = self.get_object() # call check_object_permissions as well
serializer = TaskDataSerializer(db_task, data=request.data) serializer = TaskDataSerializer(db_task, data=request.data)
if serializer.is_valid(raise_exception=True): if serializer.is_valid(raise_exception=True):
@ -303,6 +384,12 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
task.create(db_task.id, serializer.data) task.create(db_task.id, serializer.data)
return Response(serializer.data, status=status.HTTP_202_ACCEPTED) 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'], @action(detail=True, methods=['GET', 'DELETE', 'PUT', 'PATCH'],
serializer_class=LabeledDataSerializer) serializer_class=LabeledDataSerializer)
def annotations(self, request, pk): 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=str(e), status=status.HTTP_400_BAD_REQUEST)
return Response(data) 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\nYou 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, @action(detail=True, methods=['GET'], serializer_class=None,
url_path='annotations/(?P<filename>[^/]+)') url_path='annotations/(?P<filename>[^/]+)')
def dump(self, request, pk, 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) filename = re.sub(r'[\\/*?:"<>|]', '_', filename)
username = request.user.username username = request.user.username
db_task = self.get_object() # call check_object_permissions as well 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) 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) @action(detail=True, methods=['GET'], serializer_class=RqStatusSerializer)
def status(self, request, pk): def status(self, request, pk):
self.get_object() # force to call check_object_permissions self.get_object() # force to call check_object_permissions
@ -430,6 +532,8 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
return response 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, @action(detail=True, methods=['GET'], serializer_class=ImageMetaSerializer,
url_path='frames/meta') url_path='frames/meta')
def data_info(self, request, pk): def data_info(self, request, pk):
@ -445,11 +549,13 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
if serializer.is_valid(raise_exception=True): if serializer.is_valid(raise_exception=True):
return Response(serializer.data) 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, @action(detail=True, methods=['GET'], serializer_class=None,
url_path='frames/(?P<frame>\d+)') url_path='frames/(?P<frame>\d+)')
def frame(self, request, pk, frame): def frame(self, request, pk, frame):
"""Get a frame for the task"""
try: try:
# Follow symbol links if the frame is a link on a real image otherwise # Follow symbol links if the frame is a link on a real image otherwise
# mimetype detection inside sendfile will work incorrectly. # 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) "cannot get frame #{}".format(frame), exc_info=True)
return HttpResponseBadRequest(str(e)) 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, @action(detail=True, methods=['GET'], serializer_class=None,
url_path='dataset') url_path='dataset')
def dataset_export(self, request, pk): def dataset_export(self, request, pk):
"""Export task as a dataset in a specific format"""
db_task = self.get_object() db_task = self.get_object()
@ -528,6 +640,10 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
result_ttl=ttl, failure_ttl=ttl) result_ttl=ttl, failure_ttl=ttl)
return Response(status=status.HTTP_202_ACCEPTED) 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, class JobViewSet(viewsets.GenericViewSet,
mixins.RetrieveModelMixin, mixins.UpdateModelMixin): mixins.RetrieveModelMixin, mixins.UpdateModelMixin):
queryset = Job.objects.all().order_by('id') queryset = Job.objects.all().order_by('id')
@ -546,7 +662,13 @@ class JobViewSet(viewsets.GenericViewSet,
return [perm() for perm in permissions] 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'], @action(detail=True, methods=['GET', 'DELETE', 'PUT', 'PATCH'],
serializer_class=LabeledDataSerializer) serializer_class=LabeledDataSerializer)
def annotations(self, request, pk): 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=str(e), status=status.HTTP_400_BAD_REQUEST)
return Response(data) 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, class UserViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin): mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin):
queryset = User.objects.all().order_by('id') queryset = User.objects.all().order_by('id')
@ -615,8 +745,12 @@ class UserViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
return [perm() for perm in permissions] 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']) @action(detail=False, methods=['GET'])
def self(self, request): def self(self, request):
"""
Method returns an instance of a user who is currently authorized
"""
serializer_class = self.get_serializer_class() serializer_class = self.get_serializer_class()
serializer = serializer_class(request.user, context={ "request": request }) serializer = serializer_class(request.user, context={ "request": request })
return Response(serializer.data) return Response(serializer.data)
@ -657,6 +791,15 @@ def rq_handler(job, exc_type, exc_value, tb):
return True 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): def load_data_proxy(request, rq_id, rq_func, pk):
queue = django_rq.get_queue("default") queue = django_rq.get_queue("default")
rq_job = queue.fetch_job(rq_id) rq_job = queue.fetch_job(rq_id)

Loading…
Cancel
Save