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