diff --git a/label_studio/projects/serializers.py b/label_studio/projects/serializers.py
index c54be0470f5b..1df08fdd524a 100644
--- a/label_studio/projects/serializers.py
+++ b/label_studio/projects/serializers.py
@@ -3,6 +3,30 @@
import bleach
from constants import SAFE_HTML_ATTRIBUTES, SAFE_HTML_TAGS
from django.db.models import Q
+from label_studio_sdk.label_interface import LabelInterface
+from label_studio_sdk.label_interface.control_tags import (
+ BrushLabelsTag,
+ BrushTag,
+ ChoicesTag,
+ DateTimeTag,
+ EllipseLabelsTag,
+ EllipseTag,
+ HyperTextLabelsTag,
+ KeyPointLabelsTag,
+ KeyPointTag,
+ LabelsTag,
+ NumberTag,
+ ParagraphLabelsTag,
+ PolygonLabelsTag,
+ PolygonTag,
+ RatingTag,
+ RectangleLabelsTag,
+ RectangleTag,
+ TaxonomyTag,
+ TextAreaTag,
+ TimeSeriesLabelsTag,
+ VideoRectangleTag,
+)
from projects.models import Project, ProjectImport, ProjectOnboarding, ProjectReimport, ProjectSummary
from rest_flex_fields import FlexFieldsModelSerializer
from rest_framework import serializers
@@ -66,6 +90,9 @@ class ProjectSerializer(FlexFieldsModelSerializer):
config_has_control_tags = SerializerMethodField(
default=None, read_only=True, help_text='Flag to detect is project ready for labeling'
)
+ config_suitable_for_bulk_annotation = serializers.SerializerMethodField(
+ default=None, read_only=True, help_text='Flag to detect is project ready for bulk annotation'
+ )
finished_task_number = serializers.IntegerField(default=None, read_only=True, help_text='Finished tasks')
queue_total = serializers.SerializerMethodField()
@@ -82,6 +109,61 @@ def user_id(self):
def get_config_has_control_tags(project):
return len(project.get_parsed_config()) > 0
+ @staticmethod
+ def get_config_suitable_for_bulk_annotation(project):
+ li = LabelInterface(project.label_config)
+
+ # List of tags that should not be present
+ disallowed_tags = [
+ LabelsTag,
+ BrushTag,
+ BrushLabelsTag,
+ EllipseTag,
+ EllipseLabelsTag,
+ KeyPointTag,
+ KeyPointLabelsTag,
+ PolygonTag,
+ PolygonLabelsTag,
+ RectangleTag,
+ RectangleLabelsTag,
+ HyperTextLabelsTag,
+ ParagraphLabelsTag,
+ TimeSeriesLabelsTag,
+ VideoRectangleTag,
+ ]
+
+ # Return False if any disallowed tag is present
+ for tag_class in disallowed_tags:
+ if li.find_tags_by_class(tag_class):
+ return False
+
+ # Check perRegion/perItem for expanded list of tags, plus value="no" for Choices/Taxonomy
+ allowed_tags_for_checks = [ChoicesTag, TaxonomyTag, DateTimeTag, NumberTag, RatingTag, TextAreaTag]
+ for tag_class in allowed_tags_for_checks:
+ tags = li.find_tags_by_class(tag_class)
+ for tag in tags:
+ per_region = tag.attr.get('perRegion', 'false').lower() == 'true'
+ per_item = tag.attr.get('perItem', 'false').lower() == 'true'
+ if per_region or per_item:
+ return False
+ # For ChoicesTag and TaxonomyTag, the value attribute must not be set at all
+ if tag_class in [ChoicesTag, TaxonomyTag]:
+ if 'value' in tag.attr:
+ return False
+
+ # For TaxonomyTag, check labeling and apiUrl
+ taxonomy_tags = li.find_tags_by_class(TaxonomyTag)
+ for tag in taxonomy_tags:
+ labeling = tag.attr.get('labeling', 'false').lower() == 'true'
+ if labeling:
+ return False
+ api_url = tag.attr.get('apiUrl', None)
+ if api_url is not None:
+ return False
+
+ # If all checks pass, return True
+ return True
+
@staticmethod
def get_parsed_label_config(project):
return project.get_parsed_config()
@@ -156,6 +238,7 @@ class Meta:
'finished_task_number',
'queue_total',
'queue_done',
+ 'config_suitable_for_bulk_annotation',
]
def validate_label_config(self, value):
diff --git a/label_studio/tests/config_validation.tavern.yml b/label_studio/tests/config_validation.tavern.yml
index 3dca2afeff3b..0320c072b97d 100644
--- a/label_studio/tests/config_validation.tavern.yml
+++ b/label_studio/tests/config_validation.tavern.yml
@@ -1435,4 +1435,73 @@ stages:
method: POST
url: '{django_live_url}/api/projects/{pk}/validate'
response:
- status_code: 200
\ No newline at end of file
+ status_code: 200
+
+---
+test_name: check_config_suitable_for_bulk_annotation
+strict: false
+marks:
+- usefixtures:
+ - django_live_url
+stages:
+
+- id: signup
+ type: ref
+
+- name: create classification project
+ request:
+ data:
+ label_config: |
+
+
+
+
+
+
+
+
+ method: POST
+ url: '{django_live_url}/api/projects'
+ response:
+ status_code: 201
+ save:
+ json:
+ classification_project_id: id
+
+- name: check classification project property
+ request:
+ method: GET
+ url: '{django_live_url}/api/projects/{classification_project_id}'
+ response:
+ status_code: 200
+ json:
+ config_suitable_for_bulk_annotation: true
+
+- name: create object detection project
+ request:
+ data:
+ label_config: |
+
+
+
+
+
+
+
+
+ method: POST
+ url: '{django_live_url}/api/projects'
+ response:
+ status_code: 201
+ save:
+ json:
+ detection_project_id: id
+
+- name: check object detection project property
+ request:
+ method: GET
+ url: '{django_live_url}/api/projects/{detection_project_id}'
+ response:
+ status_code: 200
+ json:
+ config_suitable_for_bulk_annotation: false