diff --git a/cvat/apps/engine/log.py b/cvat/apps/engine/log.py index 4b065bfc6c3..d54e161a73c 100644 --- a/cvat/apps/engine/log.py +++ b/cvat/apps/engine/log.py @@ -90,7 +90,8 @@ class dotdict(dict): clogger = dotdict({ 'task': TaskClientLoggerStorage(), - 'job': JobClientLoggerStorage() + 'job': JobClientLoggerStorage(), + 'glob': logging.getLogger('cvat.client'), }) slogger = dotdict({ diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index be512bea4fd..86cdb5c2b97 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -9,16 +9,25 @@ class Meta: 'bug_tracker', 'created_date', 'updated_date', 'overlap', 'z_order', 'flipped', 'source', 'status') -class GroupSerializer(serializers.ModelSerializer): - class Meta: - model = Group - fields = ('name',) - class UserSerializer(serializers.ModelSerializer): - groups = GroupSerializer(many=True) + groups = serializers.SlugRelatedField(many=True, + slug_field='name', queryset=Group.objects.all()) class Meta: model = User - fields = ('id', 'first_name', 'last_name', 'email', 'is_staff', - 'is_superuser', 'is_active', 'groups') - write_only_fields = ('password', 'date_joined') + fields = ('id', 'username', 'first_name', 'last_name', 'email', + 'groups', 'is_staff', 'is_superuser', 'is_active', 'last_login', + 'date_joined', 'groups') + read_only_fields = ('last_login', 'date_joined') + write_only_fields = ('password', ) + +class ExceptionSerializer(serializers.Serializer): + task = serializers.IntegerField() + job = serializers.IntegerField() + message = serializers.CharField(max_length=1000) + filename = serializers.URLField() + line = serializers.IntegerField() + column = serializers.IntegerField() + stack = serializers.CharField(max_length=10000) + browser = serializers.CharField(max_length=255) + os = serializers.CharField(max_length=255) diff --git a/cvat/apps/engine/urls.py b/cvat/apps/engine/urls.py index 42b13bf9acc..21e36d48b26 100644 --- a/cvat/apps/engine/urls.py +++ b/cvat/apps/engine/urls.py @@ -9,10 +9,29 @@ REST_API_PREFIX = 'api//' urlpatterns = [ - path( # entry point for API - REST_API_PREFIX, - views.api_root, - name='root'), + # entry point for API + path(REST_API_PREFIX, views.api_root, name='root'), + # GET current active user + path(REST_API_PREFIX + 'users/self', views.UserSelf.as_view(), + name='user-self'), + # GET list of users, POST a new user + path(REST_API_PREFIX + 'users/', views.UserList.as_view(), + name='user-list'), + # GET, DELETE, PATCH the user + path(REST_API_PREFIX + 'users/', views.UserDetail.as_view(), + name='user-detail'), + # GET a frame for a specific task + path(REST_API_PREFIX + 'tasks//frames/', + views.get_frame, name='task-frame'), + # POST an exception + path(REST_API_PREFIX + 'exceptions/', views.ClientException.as_view(), + name='exception-list'), + + + # GET information about the backend + path(REST_API_PREFIX + 'info/', views.dummy_view, name='server-info'), + + path( # GET, POST REST_API_PREFIX + 'tasks/', views.TaskList.as_view(), @@ -21,10 +40,6 @@ REST_API_PREFIX + 'tasks/', views.TaskDetail.as_view(), name='task-detail'), - path( # GET - REST_API_PREFIX + 'tasks//frames/', - views.dummy_view, - name='task-frame'), path( # GET REST_API_PREFIX + 'tasks//jobs/', views.dummy_view, @@ -41,26 +56,6 @@ REST_API_PREFIX + 'jobs//annotations/', views.dummy_view, name='job-annotations'), - path( # GET - REST_API_PREFIX + 'users/', - views.UserList.as_view(), - name='user-list'), - path( # GET, DELETE, PATCH - REST_API_PREFIX + 'users/', - views.UserDetail.as_view(), - name='user-detail'), - path( # GET - REST_API_PREFIX + 'users/myself', - views.dummy_view, - name='user-myself'), - path( # POST - REST_API_PREFIX + 'exceptions/', - views.dummy_view, - name='exception-list'), - path( # GET - REST_API_PREFIX + 'info/', - views.dummy_view, - name='server-info'), path( # GET REST_API_PREFIX + 'plugins/', views.dummy_view, @@ -87,7 +82,7 @@ name='plugin-request-detail'), path('create/task', views.create_task), #### - path('get/task//frame/', views.get_frame), ### + path('get/task//frame/', views.get_frame), ### path('check/task/', views.check_task), #### path('delete/task/', views.delete_task), #### path('update/task/', views.update_task), #### diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 1a4133a985b..a2a6ccf9862 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -17,6 +17,8 @@ from rest_framework.decorators import api_view, APIView from rest_framework.response import Response from rest_framework.reverse import reverse +from rest_framework.renderers import JSONRenderer + from . import annotation, task, models @@ -26,7 +28,8 @@ import logging from .log import slogger, clogger from cvat.apps.engine.models import StatusChoice, Task -from cvat.apps.engine.serializers import TaskSerializer, UserSerializer +from cvat.apps.engine.serializers import TaskSerializer, UserSerializer,\ + ExceptionSerializer from django.contrib.auth.models import User # Server REST API @@ -37,7 +40,7 @@ def api_root(request, version=None): return Response({ 'tasks': reverse('task-list', request=request), 'users': reverse('user-list', request=request), - 'myself': reverse('user-myself', request=request), + 'myself': reverse('user-self', request=request), 'exceptions': reverse('exception-list', request=request), 'info': reverse('server-info', request=request), 'plugins': reverse('plugin-list', request=request) @@ -48,28 +51,75 @@ class TaskList(generics.ListCreateAPIView): queryset = Task.objects.all() serializer_class = TaskSerializer + class TaskDetail(generics.RetrieveUpdateDestroyAPIView): queryset = Task.objects.all() serializer_class = TaskSerializer + class UserList(generics.ListCreateAPIView): queryset = User.objects.all() serializer_class = UserSerializer + class UserDetail(generics.RetrieveUpdateDestroyAPIView): queryset = User.objects.all() serializer_class = UserSerializer + +class UserSelf(generics.RetrieveAPIView): + serializer_class = UserSerializer + + def get_object(self): + return self.request.user + + +@login_required +@permission_required(perm=['engine.task.access'], + fn=objectgetter(models.Task, 'pk'), raise_exception=True) +def get_frame(request, pk, frame, version=None): + """Stream corresponding from 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. + path = os.path.realpath(task.get_frame_path(pk, frame)) + return sendfile(request, path) + except Exception as e: + slogger.task[pk].error( + "cannot get frame #{}".format(frame), exc_info=True) + return HttpResponseBadRequest(str(e)) + + +class ClientException(APIView): + def post(self, request): + serializer = ExceptionSerializer(data=request.data) + if serializer.is_valid(raise_exception=True): + message = JSONRenderer().render(serializer.data) + jid = serializer.data["job"] + tid = serializer.data["task"] + if jid: + clogger.job[jid].error(message) + elif tid: + clogger.task[tid].error(message) + else: + clogger.glob.error(message) + + return Response(serializer.data, status=status.HTTP_201_CREATED) + + @api_view(['GET']) def dummy_view(request, version=None, pk=None, frame=None, id=None, name=None): return Response() + +# XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX # High Level server API @login_required @permission_required(perm=['engine.job.access'], - fn=objectgetter(models.Job, 'jid'), raise_exception=True) + fn=objectgetter(models.Job, 'jid'), raise_exception=True) def catch_client_exception(request, jid): data = json.loads(request.body.decode('utf-8')) for event in data['exceptions']: @@ -169,23 +219,6 @@ def check_task(request, tid): return JsonResponse(response) -@login_required -@permission_required(perm=['engine.task.access'], - fn=objectgetter(models.Task, 'tid'), raise_exception=True) -def get_frame(request, tid, frame): - """Stream corresponding from 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. - path = os.path.realpath(task.get_frame_path(tid, frame)) - return sendfile(request, path) - except Exception as e: - slogger.task[tid].error( - "cannot get frame #{}".format(frame), exc_info=True) - return HttpResponseBadRequest(str(e)) - - @login_required @permission_required(perm=['engine.task.delete'], fn=objectgetter(models.Task, 'tid'), raise_exception=True)