Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion label_studio/data_export/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from django.core.files import File
from django.core.files.storage import FileSystemStorage
from django.db import transaction
from django.db.models import Prefetch
from django.http import FileResponse, HttpResponse
from django.utils.decorators import method_decorator
from drf_spectacular.types import OpenApiTypes
Expand Down Expand Up @@ -166,7 +167,34 @@ def get_queryset(self):
return Project.objects.filter(organization=self.request.user.active_organization)

def get_task_queryset(self, queryset):
return queryset.select_related('project').prefetch_related('annotations', 'predictions')
# Import here to avoid circular dependencies
from core.feature_flags import flag_set
from tasks.models import Annotation

# Create a prefetch for annotations with FSM state
annotations_qs = Annotation.objects.all()

# Only annotate FSM state if both feature flags are enabled
user = getattr(self.request, 'user', None)
if (
flag_set('fflag_feat_fit_568_finite_state_management', user=user)
and flag_set('fflag_feat_fit_710_fsm_state_fields', user=user)
and hasattr(annotations_qs, 'with_state')
):
annotations_qs = annotations_qs.with_state()

qs = queryset.select_related('project').prefetch_related(
Prefetch('annotations', queryset=annotations_qs), 'predictions'
)

# Add FSM state annotation to tasks as well to avoid N+1 queries during export
if (
flag_set('fflag_feat_fit_568_finite_state_management', user=user)
and flag_set('fflag_feat_fit_710_fsm_state_fields', user=user)
and hasattr(qs, 'with_state')
):
qs = qs.with_state()
return qs

def get(self, request, *args, **kwargs):
project = self.get_object()
Expand Down
24 changes: 23 additions & 1 deletion label_studio/data_export/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,21 @@ def _get_export_serializer_option(serialization_options):
return options

def get_task_queryset(self, ids, annotation_filter_options):
from core.feature_flags import flag_set

annotations_qs = self._get_filtered_annotations_queryset(annotation_filter_options=annotation_filter_options)

return (
# Only annotate FSM state if both feature flags are enabled
# This prevents unnecessary query annotations when state won't be serialized
user = getattr(self, 'created_by', None)
if (
flag_set('fflag_feat_fit_568_finite_state_management', user=user)
and flag_set('fflag_feat_fit_710_fsm_state_fields', user=user)
and hasattr(annotations_qs, 'with_state')
):
annotations_qs = annotations_qs.with_state()

qs = (
Task.objects.filter(id__in=ids)
.select_related('file_upload') # select_related more efficient for regular foreign-key relationship
.prefetch_related(
Expand All @@ -164,6 +176,16 @@ def get_task_queryset(self, ids, annotation_filter_options):
)
)

# Add FSM state annotation to tasks as well to avoid N+1 queries during export
if (
flag_set('fflag_feat_fit_568_finite_state_management', user=user)
and flag_set('fflag_feat_fit_710_fsm_state_fields', user=user)
and hasattr(qs, 'with_state')
):
qs = qs.with_state()

return qs

def get_export_data(self, task_filter_options=None, annotation_filter_options=None, serialization_options=None):
"""
serialization_options: None or Dict({
Expand Down
35 changes: 34 additions & 1 deletion label_studio/data_export/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from core.utils.common import load_func
from data_export.models import DataExport
from django.conf import settings
from fsm.serializer_fields import FSMStateField
from label_studio_sdk._extensions.label_studio_tools.core.label_config import is_video_object_tracking
from label_studio_sdk._extensions.label_studio_tools.postprocessing.video import extract_key_frames
from ml.mixins import InteractiveMixin
Expand All @@ -26,12 +27,30 @@ class Meta:
class AnnotationSerializer(FlexFieldsModelSerializer):
completed_by = serializers.PrimaryKeyRelatedField(read_only=True)
result = serializers.SerializerMethodField()
state = FSMStateField(read_only=True) # FSM state for annotations

class Meta:
model = Annotation
fields = '__all__'
expandable_fields = {'completed_by': (CompletedBySerializer,)}

def to_representation(self, instance):
"""Override to conditionally exclude FSM state field when feature flags are disabled."""
from core.current_request import CurrentContext
from core.feature_flags import flag_set

ret = super().to_representation(instance)

# Remove state field from output if either feature flag is disabled
user = CurrentContext.get_user()
if not (
flag_set('fflag_feat_fit_568_finite_state_management', user=user)
and flag_set('fflag_feat_fit_710_fsm_state_fields', user=user)
):
ret.pop('state', None)

return ret

def get_result(self, obj):
# run frames extraction on param, result and result type
if (
Expand All @@ -48,9 +67,13 @@ class BaseExportDataSerializer(FlexFieldsModelSerializer):
file_upload = serializers.ReadOnlyField(source='file_upload_name')
drafts = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
predictions = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
state = FSMStateField(read_only=True) # FSM state for tasks

# resolve $undefined$ key in task data, if any
def to_representation(self, task):
from core.current_request import CurrentContext
from core.feature_flags import flag_set

# avoid long project initializations
project = getattr(self, '_project', None)
if project is None:
Expand All @@ -65,7 +88,17 @@ def to_representation(self, task):
)
replace_task_data_undefined_with_config_field(data, project)

return super().to_representation(task)
ret = super().to_representation(task)

# Remove state field from output if either feature flag is disabled
user = CurrentContext.get_user()
if not (
flag_set('fflag_feat_fit_568_finite_state_management', user=user)
and flag_set('fflag_feat_fit_710_fsm_state_fields', user=user)
):
ret.pop('state', None)

return ret

class Meta:
model = Task
Expand Down
3 changes: 3 additions & 0 deletions web/libs/datamanager/src/components/Common/Table/Table.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,9 @@ const TaskSourceView = ({ content, onTaskLoad, sdkType }) => {
formatted.annotations = response.annotations ?? [];
formatted.predictions = response.predictions ?? [];
}
if (response.state) {
formatted.state = response.state;
}
setSource(formatted);
});
}, []);
Expand Down
Loading