Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: RND-98: correct response schema for tasks.get() #6012

Merged
merged 15 commits into from
Jun 21, 2024
60 changes: 54 additions & 6 deletions label_studio/data_manager/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@
from data_manager.models import Filter, FilterGroup, View
from django.conf import settings
from django.db import transaction
from drf_yasg import openapi
from projects.models import Project
from rest_framework import serializers
from tasks.models import Task
from tasks.serializers import AnnotationDraftSerializer, AnnotationSerializer, PredictionSerializer, TaskSerializer
from tasks.serializers import (
AnnotationDraftSerializer,
AnnotationResultField,
AnnotationSerializer,
PredictionSerializer,
TaskSerializer,
)
from users.models import User

from label_studio.core.utils.common import round_floats

Expand Down Expand Up @@ -202,11 +210,51 @@ def update(self, instance, validated_data):
return instance


class UpdatedByDMField(serializers.SerializerMethodField):
# TODO: get_updated_by implementation is weird, but we need to adhere schema to it
class Meta:
swagger_schema_fields = {
'type': openapi.TYPE_ARRAY,
'title': 'User IDs',
'description': 'User IDs who updated this task',
'items': {'type': openapi.TYPE_OBJECT, 'title': 'User IDs'},
}


class AnnotatorsDMField(serializers.SerializerMethodField):
# TODO: get_updated_by implementation is weird, but we need to adhere schema to it
class Meta:
swagger_schema_fields = {
'type': openapi.TYPE_ARRAY,
'title': 'Annotators IDs',
'description': 'Annotators IDs who annotated this task',
'items': {'type': openapi.TYPE_INTEGER, 'title': 'User IDs'},
}


class CompletedByDMSerializerWithGenericSchema(serializers.PrimaryKeyRelatedField):
# TODO: likely we need to remove full user details from GET /api/tasks/{id} as it non-secure and currently controlled by the export toggle
class Meta:
swagger_schema_fields = {
'type': openapi.TYPE_OBJECT,
'title': 'User details',
'description': 'User details who completed this annotation.',
}


class AnnotationsDMFieldSerializer(AnnotationSerializer):
completed_by = CompletedByDMSerializerWithGenericSchema(required=False, queryset=User.objects.all())


class AnnotationDraftDMFieldSerializer(AnnotationDraftSerializer):
result = AnnotationResultField(required=False)


class DataManagerTaskSerializer(TaskSerializer):
predictions = serializers.SerializerMethodField(required=False, read_only=True)
annotations = AnnotationSerializer(required=False, many=True, default=[], read_only=True)
drafts = serializers.SerializerMethodField(required=False, read_only=True)
annotators = serializers.SerializerMethodField(required=False, read_only=True)
predictions = PredictionSerializer(required=False, many=True, default=[], read_only=True)
annotations = AnnotationsDMFieldSerializer(required=False, many=True, default=[], read_only=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might affect the calls of LseTaskSerializer::get_annotations() and LseTaskSerializerForAnnotators::get_annotations() from LSE

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

drafts = AnnotationDraftDMFieldSerializer(required=False, many=True, default=[], read_only=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will break the call of DataManagerTaskSerializer::get_drafts() + LseTaskSerializer::get_drafts()

some-2024-06-19_00.20.54.mp4

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed for predictions and drafts - schemas were rewritten manually on top of serializers.SerializerMethodField

annotators = AnnotatorsDMField(required=False, read_only=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the same problem as with get_drafts() + LseTaskSerializer::get_annotators()


inner_id = serializers.IntegerField(required=False)
cancelled_annotations = serializers.IntegerField(required=False)
Expand All @@ -222,7 +270,7 @@ class DataManagerTaskSerializer(TaskSerializer):
predictions_model_versions = serializers.SerializerMethodField(required=False)
avg_lead_time = serializers.FloatField(required=False)
draft_exists = serializers.BooleanField(required=False)
updated_by = serializers.SerializerMethodField(required=False, read_only=True)
updated_by = UpdatedByDMField(required=False, read_only=True)
Copy link
Member

@makseq makseq Jun 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the same problem as with get_drafts()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure the problem does appear for this field as well? As it is just as shallow override over serializers.SerializerMethodField


CHAR_LIMITS = 500

Expand Down
4 changes: 3 additions & 1 deletion label_studio/tasks/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,9 @@ def perform_create(self, serializer):
request_body=no_body,
responses={
'200': openapi.Response(
description='Task', schema=TaskSerializer, examples={'application/json': task_response_example}
description='Task',
schema=DataManagerTaskSerializer,
examples={'application/json': task_response_example},
)
},
),
Expand Down
4 changes: 2 additions & 2 deletions label_studio/tasks/openapi_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
'comment_count': 0,
'unresolved_comment_count': 0,
'last_comment_updated_at': '2024-01-15T09:30:00Z',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it should be like this:
2024-06-18T23:45:46.048490Z

pls, fix created_at and updated_at too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe it's better to use the full version of api/tasks response:

{
  "id": 13,
  "predictions": [],
  "annotations": [],
  "drafts": [],
  "annotators": [],
  "inner_id": 2,
  "cancelled_annotations": 0,
  "total_annotations": 0,
  "total_predictions": 0,
  "completed_at": null,
  "annotations_results": "",
  "predictions_results": "",
  "predictions_score": null,
  "file_upload": "6b25fc23-some_3.mp4",
  "storage_filename": null,
  "annotations_ids": "",
  "predictions_model_versions": "",
  "avg_lead_time": null,
  "draft_exists": false,
  "updated_by": [],
  "data": {
    "image": "/data/upload/1/6b25fc23-some_3.mp4"
  },
  "meta": {},
  "created_at": "2024-06-18T23:45:46.048490Z",
  "updated_at": "2024-06-18T23:45:46.048538Z",
  "is_labeled": false,
  "overlap": 1,
  "comment_count": 0,
  "unresolved_comment_count": 0,
  "last_comment_updated_at": null,
  "project": 1,
  "comment_authors": []
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

'updated_by': 1,
'file_upload': 1,
'updated_by': [{'user_id': 1}],
'file_upload': '42d46c4c-my-pic.jpeg',
'comment_authors': [1],
}

Expand Down
31 changes: 19 additions & 12 deletions label_studio/tests/sdk/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,19 @@ def test_delete_multi_tasks(django_live_url, business_client):
ls.actions.create(project=p.id, id='delete_tasks', selected_items={'all': False, 'included': tasks_ids_to_delete})
assert len([task for task in ls.tasks.list(project=p.id)]) == 5

ls.actions.create(project=p.id, id='delete_tasks', selected_items={'all': True, 'excluded': [tasks[5].id]})
# another way of calling delete action
# ls.actions.create(request_options={
# 'additional_query_parameters': {
# 'project': p.id,
# 'id': 'delete_tasks'
# },
# 'additional_body_parameters': {
# 'selectedItems': {"all": True, "excluded": [tasks[5].id]},
# }
# })
# another way of calling delete action instead of
# ls.actions.create(project=p.id, id='delete_tasks', selected_items={'all': True, 'excluded': [tasks[5].id]})
import json

ls.actions.create(
project=p.id,
id='delete_tasks',
request_options={
'additional_body_parameters': {
'selectedItems': json.dumps({'all': True, 'excluded': [tasks[5].id]}),
},
},
)

remaining_tasks = [task for task in ls.tasks.list(project=p.id)]
assert len(remaining_tasks) == 1
Expand Down Expand Up @@ -94,7 +96,12 @@ def test_export_tasks(django_live_url, business_client):
}
ls.annotations.create(id=task_id, **annotation_data)

# by default, only tasks with annotations are exported
# export a singleton task
single_task = ls.tasks.get(id=task_id)
assert single_task.data['my_text'] == 'Test task 7'
assert single_task.total_annotations == 1
assert single_task.updated_by == [{'user_id': business_client.user.id}]

exported_tasks = [task for task in ls.tasks.list(project=p.id, fields='all') if task.annotations]
assert len(exported_tasks) == 1
assert exported_tasks[0].data['my_text'] == 'Test task 7'
Expand Down
Loading