diff --git a/CHANGELOG.md b/CHANGELOG.md index e7456615e981099d41f469b940acd75c5947f123..c8127641d5725e9550026381944ca88f035e3504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Exporting project when its tasks has not data () - Removing job assignee () - Fixed switching from organization to sandbox while getting a resource () - \[SDK\]: `FileExistsError` exception raised on Windows when a dataset is loaded from cache diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 6236fbbe0d1d6d21953601633147855dc2435bac..d1f52e7954641cd68ac69fbfce694c1afb021f8a 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -949,7 +949,10 @@ class ProjectData(InstanceLabelData): def _init_tasks(self): self._db_tasks: OrderedDict[int, Task] = OrderedDict( - ((db_task.id, db_task) for db_task in self._db_project.tasks.order_by("subset","id").all()) + ( + (db_task.id, db_task) + for db_task in self._db_project.tasks.exclude(data=None).order_by("subset","id").all() + ) ) subsets = set() diff --git a/cvat/apps/dataset_manager/project.py b/cvat/apps/dataset_manager/project.py index d415f4beec5ee6c7648350073c057e71df28e84e..215a4ca8de29ca2ce677870412c6fe693fd2a046 100644 --- a/cvat/apps/dataset_manager/project.py +++ b/cvat/apps/dataset_manager/project.py @@ -38,7 +38,7 @@ def export_project(project_id, dst_file, format_name, class ProjectAnnotationAndData: def __init__(self, pk: int): self.db_project = models.Project.objects.get(id=pk) - self.db_tasks = models.Task.objects.filter(project__id=pk).order_by('id') + self.db_tasks = models.Task.objects.filter(project__id=pk).exclude(data=None).order_by('id') self.task_annotations: dict[int, TaskAnnotation] = dict() self.annotation_irs: dict[int, AnnotationIR] = dict() @@ -98,7 +98,7 @@ class ProjectAnnotationAndData: data['server_files'] = list(map(split_name, data['server_files'])) create_task(db_task, data, isDatasetImport=True) - self.db_tasks = models.Task.objects.filter(project__id=self.db_project.id).order_by('id') + self.db_tasks = models.Task.objects.filter(project__id=self.db_project.id).exclude(data=None).order_by('id') self.init_from_db() if project_data is not None: project_data.new_tasks.add(db_task.id) diff --git a/cvat/apps/engine/backup.py b/cvat/apps/engine/backup.py index f33c789b074e72b832b9b3f6d064e73fea41c437..27172ab29d170510bf7058811a5c7cdf4bc234fb 100644 --- a/cvat/apps/engine/backup.py +++ b/cvat/apps/engine/backup.py @@ -780,7 +780,8 @@ class ProjectExporter(_ExporterBase, _ProjectBackupBase): def _write_tasks(self, zip_object): for idx, db_task in enumerate(self._db_project.tasks.all().order_by('id')): - TaskExporter(db_task.id, self._version).export_to(zip_object, self.TASKNAME_TEMPLATE.format(idx)) + if db_task.data is not None: + TaskExporter(db_task.id, self._version).export_to(zip_object, self.TASKNAME_TEMPLATE.format(idx)) def _write_manifest(self, zip_object): def serialize_project(): diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 23f453e3356b7a2f53c6f5a05d8debc80db9b114..52b37c4da4aade05e09b8b025201c4930984abbc 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -870,9 +870,15 @@ class TaskViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, '200': OpenApiResponse(description='Download of file started'), '201': OpenApiResponse(description='Output backup file is ready for downloading'), '202': OpenApiResponse(description='Creating a backup file has been started'), + '400': OpenApiResponse(description='Backup of a task without data is not allowed'), }) @action(methods=['GET'], detail=True, url_path='backup') def export_backup(self, request, pk=None): + if self.get_object().data is None: + return Response( + data='Backup of a task without data is not allowed', + status=status.HTTP_400_BAD_REQUEST + ) return self.serialize(request, backup.export) @transaction.atomic diff --git a/cvat/schema.yml b/cvat/schema.yml index 561036b33695bbb4f5e198475f4394d0515309a8..9adf521d56fe400233f59c24f2bcf84224f559ea 100644 --- a/cvat/schema.yml +++ b/cvat/schema.yml @@ -4917,6 +4917,8 @@ paths: description: Output backup file is ready for downloading '202': description: Creating a backup file has been started + '400': + description: Backup of a task without data is not allowed /api/tasks/{id}/data/: get: operationId: tasks_retrieve_data diff --git a/tests/python/rest_api/test_projects.py b/tests/python/rest_api/test_projects.py index 9b79ca415e6086bea9bf2a706e391c2c91ac7c33..64614124c6f944209490f093854f02dea73951c1 100644 --- a/tests/python/rest_api/test_projects.py +++ b/tests/python/rest_api/test_projects.py @@ -310,6 +310,37 @@ class TestGetProjectBackup: self._test_can_get_project_backup(user["username"], project["id"]) + def test_can_get_backup_project_when_some_tasks_have_no_data(self, projects): + project = next((p for p in projects if 0 < p["tasks"]["count"])) + + # add empty task to project + response = post_method( + "admin1", "tasks", {"name": "empty_task", "project_id": project["id"]} + ) + assert response.status_code == HTTPStatus.CREATED + + self._test_can_get_project_backup("admin1", project["id"]) + + def test_can_get_backup_project_when_all_tasks_have_no_data(self, projects): + project = next((p for p in projects if 0 == p["tasks"]["count"])) + + # add empty tasks to empty project + response = post_method( + "admin1", "tasks", {"name": "empty_task1", "project_id": project["id"]} + ) + assert response.status_code == HTTPStatus.CREATED + + response = post_method( + "admin1", "tasks", {"name": "empty_task2", "project_id": project["id"]} + ) + assert response.status_code == HTTPStatus.CREATED + + self._test_can_get_project_backup("admin1", project["id"]) + + def test_can_get_backup_for_empty_project(self, projects): + empty_project = next((p for p in projects if 0 == p["tasks"]["count"])) + self._test_can_get_project_backup("admin1", empty_project["id"]) + @pytest.mark.usefixtures("restore_db_per_function") class TestPostProjects: @@ -703,6 +734,37 @@ class TestImportExportDatasetProject: self._test_export_project(username, project_id, "COCO Keypoints 1.0") + def test_can_export_dataset_for_empty_project(self, projects): + empty_project = next((p for p in projects if 0 == p["tasks"]["count"])) + self._test_export_project("admin1", empty_project["id"], "COCO 1.0") + + def test_can_export_project_dataset_when_some_tasks_have_no_data(self, projects): + project = next((p for p in projects if 0 < p["tasks"]["count"])) + + # add empty task to project + response = post_method( + "admin1", "tasks", {"name": "empty_task", "project_id": project["id"]} + ) + assert response.status_code == HTTPStatus.CREATED + + self._test_export_project("admin1", project["id"], "COCO 1.0") + + def test_can_export_project_dataset_when_all_tasks_have_no_data(self, projects): + project = next((p for p in projects if 0 == p["tasks"]["count"])) + + # add empty tasks to empty project + response = post_method( + "admin1", "tasks", {"name": "empty_task1", "project_id": project["id"]} + ) + assert response.status_code == HTTPStatus.CREATED + + response = post_method( + "admin1", "tasks", {"name": "empty_task2", "project_id": project["id"]} + ) + assert response.status_code == HTTPStatus.CREATED + + self._test_export_project("admin1", project["id"], "COCO 1.0") + @pytest.mark.usefixtures("restore_db_per_function") class TestPatchProjectLabel: diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index 98c443b290bd993312bdc90655693bcc40d96e03..8040777012e1714f1f330484bc1d08cfd81195ec 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -21,7 +21,7 @@ from typing import List, Optional import pytest from cvat_sdk import Client, Config, exceptions from cvat_sdk.api_client import models -from cvat_sdk.api_client.api_client import ApiClient, Endpoint +from cvat_sdk.api_client.api_client import ApiClient, ApiException, Endpoint from cvat_sdk.core.helpers import get_paginated_collection from cvat_sdk.core.proxies.tasks import ResourceType, Task from cvat_sdk.core.uploading import Uploader @@ -1638,6 +1638,18 @@ class TestTaskBackups: assert filename.is_file() assert filename.stat().st_size > 0 + def test_cannot_export_backup_for_task_without_data(self, tasks): + task_id = next(t for t in tasks if t["jobs"]["count"] == 0)["id"] + task = self.client.tasks.retrieve(task_id) + + filename = self.tmp_dir / f"task_{task.id}_backup.zip" + + with pytest.raises(ApiException) as exc: + task.download_backup(filename) + + assert exc.status == HTTPStatus.BAD_REQUEST + assert "Backup of a task without data is not allowed" == exc.body.encode() + @pytest.mark.parametrize("mode", ["annotation", "interpolation"]) def test_can_import_backup(self, tasks, mode): task_json = next(t for t in tasks if t["mode"] == mode)