Bin
2025-12-17 d616898802dfe7e5dd648bcf53c6d1f86b6d3642
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
"""This file and its contents are licensed under the Apache License 2.0. Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
"""
import glob
import io
import json
import logging
import os
 
import pytest
import yaml
from core.label_config import parse_config, parse_config_to_json, validate_label_config
from projects.models import Project
 
from label_studio.tests.utils import make_annotation, make_prediction, make_task, project_id  # noqa
 
logger = logging.getLogger(__name__)
 
 
@pytest.mark.parametrize(
    'tasks_count, annotations_count, predictions_count',
    [
        [2, 2, 2],
    ],
)
@pytest.mark.django_db
def test_change_label_config_repeater(tasks_count, annotations_count, predictions_count, business_client, project_id):
    # Change label config to Repeater
    payload = {
        'label_config': '<View> <Repeater on="$images" indexFlag="{{idx}}"> <Image name="page_{{idx}}" value="$images" maxWidth="100%"/>     <Header value="Utterance Review"/>     <RectangleLabels name="labels_{{idx}}" toName="page_{{idx}}">       <Label value="Header" hotkey="1"/> <Label value="Body" hotkey="2"/> <Label value="Footer" hotkey="3"/> </RectangleLabels> </Repeater> </View>'
    }
    response = business_client.patch(
        f'/api/projects/{project_id}',
        data=json.dumps(payload),
        content_type='application/json',
    )
    assert response.status_code == 200
    # cr
    project = Project.objects.get(pk=project_id)
    for _ in range(0, tasks_count):
        task_id = make_task(
            {
                'data': {
                    'images': [
                        {'url': 'https://htx-pub.s3.amazonaws.com/demo/images/demo_stock_purchase_agreement/0001.jpg'},
                        {'url': 'https://htx-pub.s3.amazonaws.com/demo/images/demo_stock_purchase_agreement/0002.jpg'},
                        {'url': 'https://htx-pub.s3.amazonaws.com/demo/images/demo_stock_purchase_agreement/0003.jpg'},
                    ]
                }
            },
            project,
        ).id
        print('TASK_ID: %s' % task_id)
        for _ in range(0, annotations_count):
            print('COMPLETION')
            make_annotation(
                {
                    'result': [
                        {
                            'id': '_565WKjviN',
                            'type': 'rectanglelabels',
                            'value': {
                                'x': 21.451104100946377,
                                'y': 7.682926829268292,
                                'width': 54.73186119873817,
                                'height': 4.146341463414634,
                                'rotation': 0,
                                'rectanglelabels': ['Header'],
                            },
                            'origin': 'manual',
                            'to_name': 'page_0',
                            'from_name': 'labels_0',
                            'image_rotation': 0,
                            'original_width': 800,
                            'original_height': 1035,
                        }
                    ]
                },
                task_id,
            )
 
        for _ in range(0, predictions_count):
            make_prediction({'result': []}, task_id)
 
    # no changes - no errors
    payload = {
        'label_config': '<View> <Repeater on="$images" indexFlag="{{idx}}"> <Image name="page_{{idx}}" value="$images" maxWidth="100%"/>     <Header value="Utterance Review"/>     <RectangleLabels name="labels_{{idx}}" toName="page_{{idx}}"> <Label value="Header" hotkey="1"/> <Label value="Body" hotkey="2"/> <Label value="Footer" hotkey="3"/> </RectangleLabels> </Repeater> </View>'
    }
    response = business_client.post(
        f'/api/projects/{project_id}/validate',
        data=json.dumps(payload),
        content_type='application/json',
    )
    assert response.status_code == 200
 
    # delete unused labels
    payload = {
        'label_config': '<View> <Repeater on="$images" indexFlag="{{idx}}"> <Image name="page_{{idx}}" value="$images" maxWidth="100%"/>     <Header value="Utterance Review"/>     <RectangleLabels name="labels_{{idx}}" toName="page_{{idx}}"> <Label value="Header" hotkey="1"/> <Label value="Body" hotkey="2"/> </RectangleLabels> </Repeater> </View>'
    }
    response = business_client.post(
        f'/api/projects/{project_id}/validate',
        data=json.dumps(payload),
        content_type='application/json',
    )
    assert response.status_code == 200
 
    # delete used labels - 400
    payload = {
        'label_config': '<View> <Repeater on="$images" indexFlag="{{idx}}"> <Image name="page_{{idx}}" value="$images" maxWidth="100%"/>     <Header value="Utterance Review"/>     <RectangleLabels name="labels_{{idx}}" toName="page_{{idx}}"> <Label value="Body" hotkey="2"/> <Label value="Footer" hotkey="3"/> </RectangleLabels> </Repeater> </View>'
    }
    response = business_client.post(
        f'/api/projects/{project_id}/validate',
        data=json.dumps(payload),
        content_type='application/json',
    )
    assert response.status_code == 400
 
 
@pytest.mark.django_db
def test_parse_all_configs():
    folder_wildcard = './label_studio/annotation_templates'
    result = [y for x in os.walk(folder_wildcard) for y in glob.glob(os.path.join(x[0], '*.xml'))]
    for file in result:
        print(f'Parsing config: {file}')
        with open(file, mode='r') as f:
            config = f.read()
            assert parse_config(config)
            assert parse_config_to_json(config)
            validate_label_config(config)
 
 
@pytest.mark.django_db
def test_config_validation_for_choices_workaround(business_client, project_id):
    """
    Validate Choices tag for 1 choice with workaround
    Example bug DEV-3635
    """
    payload = {
        'label_config': '<View><Text value="$text" name="artist" /><View><Choices name="choices_1" toName="artist">'
        '<Choice name="choice_1" value="1"/></Choices></View><View>'
        '<Choices name="choices_2" toName="artist"><Choice name="choice_2" value="2"/></Choices>'
        '</View></View>'
    }
    response = business_client.patch(
        f'/api/projects/{project_id}',
        data=json.dumps(payload),
        content_type='application/json',
    )
    assert response.status_code == 200
 
    payload = {
        'label_config': '<View><Text value="$text" name="artist" /><View><Choices name="choices_1" toName="artist">'
        '<Choice name="choice_1" value="1"/></Choices><Choices name="choices_2" toName="artist">'
        '<Choice name="choice_2" value="2"/></Choices></View></View>'
    }
    response = business_client.patch(
        f'/api/projects/{project_id}',
        data=json.dumps(payload),
        content_type='application/json',
    )
    assert response.status_code == 200
 
 
@pytest.mark.django_db
def test_config_validation_for_missing_to_name_in_number_tag_fails(business_client, project_id):
    """
    Validate Number tag with missing to_name fails (see LEAP-245)
    """
    payload = {
        'label_config': (
            '<View>'
            '<Text name="question" value="$question" granularity="word"/>'
            '<Number name="number" to="question" required="true" />'
            '</View>'
        )
    }
    response = business_client.patch(
        f'/api/projects/{project_id}',
        data=json.dumps(payload),
        content_type='application/json',
    )
    assert response.status_code == 400
    response_data = response.json()
    assert "'toName' is a required property" in response_data['validation_errors']['label_config'][0]
 
 
@pytest.mark.django_db
def test_parse_wrong_xml(business_client, project_id):
    # Change label config to Repeater
    payload = {
        'label_config': '<View> <Repeater on="$images" indexFlag="{{idx}}"> <Image name="page_{{idx}}" value="$images" maxWidth="100%"/>     <Header value="Utterance Review"/>     <RectangleLabels name="labels_{{idx}}" toName="page_{{idx}}">       <Label value="Header" hotkey="1"/> <Label value="Body" hotkey="2"/> <Label value="Footer" hotkey="3"/> </RectangleLabels> </Repeater> </View>'
    }
    response = business_client.patch(
        f'/api/projects/{project_id}',
        data=json.dumps(payload),
        content_type='application/json',
    )
    assert response.status_code == 200
    # Change label config to wrong XML
    payload = {
        'label_config': '1<View> <Repeater on="$images" indexFlag="{{idx}}"> <Image name="page_{{idx}}" value="$images" maxWidth="100%"/>     <Header value="Utterance Review"/>     <RectangleLabels name="labels_{{idx}}" toName="page_{{idx}}"> <Label value="Body" hotkey="2"/> <Label value="Footer" hotkey="3"/> </RectangleLabels> </Repeater> </View>'
    }
    response = business_client.post(
        f'/api/projects/{project_id}/validate',
        data=json.dumps(payload),
        content_type='application/json',
    )
    assert response.status_code == 400
 
 
@pytest.mark.django_db
def test_label_config_versions(business_client, project_id):
    with io.open(os.path.join(os.path.dirname(__file__), 'test_data/data_for_test_label_config_matrix.yml')) as f:
        test_suites = yaml.safe_load(f)
    for test_name, test_content in test_suites.items():
        payload = {'label_config': test_content['label_config']}
        response = business_client.post(
            f'/api/projects/{project_id}/validate',
            data=json.dumps(payload),
            content_type='application/json',
        )
        logger.warning(f'Test: {test_name}')
        assert response.status_code == test_content['status_code']