Repository: voxel51/fiftyone

Issue:
[BUG] CVAT v2.5 text and checkbox attributes require values
CVAT v2.5 introduced a breaking change where text and checkbox attributes now require a `values` attribute to be provided. For other attribute types like radio and select, these values are the options to choose from, for text attributes, this just needs to be an empty list, for checkbox attributes it needs to be `[True, False]`

This error currently raises:
```
Exception: 400 error for request <PreparedRequest [POST]> to url http://10.2xx.2xx.2xx:8080/api/tasks with the reason Bad Request. Error content: b'{"labels":[{"attributes":[{"values":["This field is required."]},{"values":["This field is required."]},{"values":["This field is required."]}]}]}'
```


Discussion:

Resolving patch:
diff --git a/fiftyone/utils/cvat.py b/fiftyone/utils/cvat.py
--- a/fiftyone/utils/cvat.py
+++ b/fiftyone/utils/cvat.py
@@ -3260,16 +3260,29 @@ def recommend_attr_tool(self, name, value):
             if name == "occluded":
                 return {"type": "occluded"}
 
-            return {"type": "checkbox"}
+            return {"type": "checkbox", "values": [True, False]}
 
         if isinstance(value, int):
             if name == "group_id":
                 return {"type": "group_id"}
 
-        return {"type": "text"}
+        return {"type": "text", "values": []}
 
     def requires_attr_values(self, attr_type):
-        return attr_type in ("select", "radio")
+        attrs = ("select", "radio")
+        api = self.connect_to_api()
+        if api.server_version >= Version("2.5") and attr_type in (
+            "text",
+            "checkbox",
+        ):
+            logger.warning(
+                "As of CVAT v2.5, text attributes now require an empty list of"
+                " values and checkbox require a [True, False] list of values "
+                "to be provided"
+            )
+            attrs = ("select", "radio", "text", "checkbox")
+
+        return attr_type in attrs
 
     def _connect_to_api(self):
         return CVATAnnotationAPI(
@@ -4956,6 +4969,7 @@ def _convert_cvat_schema(
                     "name": "label_id",
                     "input_type": "text",
                     "mutable": True,
+                    "values": [],
                 }
 
             label_attrs = {}
@@ -5079,6 +5093,7 @@ def _build_cvat_schema(
                     "name": "label_id",
                     "input_type": "text",
                     "mutable": True,
+                    "values": [],
                 }
 
             if label_type == "scalar":
@@ -5096,6 +5111,7 @@ def _build_cvat_schema(
                         "name": "value",
                         "input_type": "text",
                         "mutable": True,
+                        "values": [],
                     }
 
             # Handle class name clashes and global attributes
@@ -5204,7 +5220,12 @@ def _add_project_label_ids(self, project_id, labels):
         labels_patch = {"labels": []}
         for label in labels:
             label["attributes"].append(
-                {"name": "label_id", "input_type": "text", "mutable": True}
+                {
+                    "name": "label_id",
+                    "input_type": "text",
+                    "mutable": True,
+                    "values": [],
+                }
             )
             labels_patch["labels"].append(label)
 


Test patch:
diff --git a/tests/intensive/cvat_tests.py b/tests/intensive/cvat_tests.py
--- a/tests/intensive/cvat_tests.py
+++ b/tests/intensive/cvat_tests.py
@@ -73,6 +73,7 @@ def _create_annotation(
     track=None,
     points=None,
     _type=None,
+    _label=None,
     group_id=0,
 ):
     if points is None:
@@ -84,7 +85,10 @@ def _create_annotation(
     tracks = []
     if shape is not None:
         if not isinstance(shape, dict):
-            label = _get_label(api, task_id, label=shape)
+            if _label is None:
+                label = _get_label(api, task_id, label=shape)
+            else:
+                label = _get_label(api, task_id, label=_label)
 
             shape = {
                 "type": _type,
@@ -99,7 +103,10 @@ def _create_annotation(
 
     if tag is not None:
         if not isinstance(tag, dict):
-            label = _get_label(api, task_id, label=tag)
+            if _label is None:
+                label = _get_label(api, task_id, label=tag)
+            else:
+                label = _get_label(api, task_id, label=_label)
             tag = {
                 "frame": 0,
                 "label_id": label,
@@ -110,7 +117,10 @@ def _create_annotation(
 
     if track is not None:
         if not isinstance(track, dict):
-            label = _get_label(api, task_id, label=track)
+            if _label is None:
+                label = _get_label(api, task_id, label=track)
+            else:
+                label = _get_label(api, task_id, label=_label)
             if isinstance(track, tuple):
                 start, end = track
             else:
@@ -289,7 +299,7 @@ def test_detection_labelling(self):
         )
 
         anno_key = "anno_key"
-        attributes = {"test": {"type": "text"}}
+        attributes = {"test": {"type": "text", "values": []}}
 
         results = dataset.annotate(
             anno_key,
@@ -639,6 +649,7 @@ def test_example_restricting_label_edits(self):
             },
             "age": {
                 "type": "text",
+                "values": [],
             },
         }
 
@@ -1415,6 +1426,7 @@ def test_frame_start_stop_step(self):
                 api,
                 task_id,
                 track=(track_start, track_end),
+                _label="test_track",
             )
             shape = {
                 "type": "rectangle",