未验证 提交 5cc8a24a 编写于 作者: K Kirill Sizov 提交者: GitHub

Ignore tasks without data during project export (#6658)

### Motivation and context
- Allow backup and export annotations for a project even if it has tasks
without data
- Add an error message on attempting to create backup of a task that has
no data
上级 1780e973
...@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ...@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Exporting project when its tasks has not data (<https://github.com/opencv/cvat/pull/6658>)
- Removing job assignee (<https://github.com/opencv/cvat/pull/6712>) - Removing job assignee (<https://github.com/opencv/cvat/pull/6712>)
- Fixed switching from organization to sandbox while getting a resource (<https://github.com/opencv/cvat/pull/6689>) - Fixed switching from organization to sandbox while getting a resource (<https://github.com/opencv/cvat/pull/6689>)
- \[SDK\]: `FileExistsError` exception raised on Windows when a dataset is loaded from cache - \[SDK\]: `FileExistsError` exception raised on Windows when a dataset is loaded from cache
......
...@@ -949,7 +949,10 @@ class ProjectData(InstanceLabelData): ...@@ -949,7 +949,10 @@ class ProjectData(InstanceLabelData):
def _init_tasks(self): def _init_tasks(self):
self._db_tasks: OrderedDict[int, Task] = OrderedDict( 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() subsets = set()
......
...@@ -38,7 +38,7 @@ def export_project(project_id, dst_file, format_name, ...@@ -38,7 +38,7 @@ def export_project(project_id, dst_file, format_name,
class ProjectAnnotationAndData: class ProjectAnnotationAndData:
def __init__(self, pk: int): def __init__(self, pk: int):
self.db_project = models.Project.objects.get(id=pk) 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.task_annotations: dict[int, TaskAnnotation] = dict()
self.annotation_irs: dict[int, AnnotationIR] = dict() self.annotation_irs: dict[int, AnnotationIR] = dict()
...@@ -98,7 +98,7 @@ class ProjectAnnotationAndData: ...@@ -98,7 +98,7 @@ class ProjectAnnotationAndData:
data['server_files'] = list(map(split_name, data['server_files'])) data['server_files'] = list(map(split_name, data['server_files']))
create_task(db_task, data, isDatasetImport=True) 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() self.init_from_db()
if project_data is not None: if project_data is not None:
project_data.new_tasks.add(db_task.id) project_data.new_tasks.add(db_task.id)
......
...@@ -780,6 +780,7 @@ class ProjectExporter(_ExporterBase, _ProjectBackupBase): ...@@ -780,6 +780,7 @@ class ProjectExporter(_ExporterBase, _ProjectBackupBase):
def _write_tasks(self, zip_object): def _write_tasks(self, zip_object):
for idx, db_task in enumerate(self._db_project.tasks.all().order_by('id')): for idx, db_task in enumerate(self._db_project.tasks.all().order_by('id')):
if db_task.data is not None:
TaskExporter(db_task.id, self._version).export_to(zip_object, self.TASKNAME_TEMPLATE.format(idx)) TaskExporter(db_task.id, self._version).export_to(zip_object, self.TASKNAME_TEMPLATE.format(idx))
def _write_manifest(self, zip_object): def _write_manifest(self, zip_object):
......
...@@ -870,9 +870,15 @@ class TaskViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, ...@@ -870,9 +870,15 @@ class TaskViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
'200': OpenApiResponse(description='Download of file started'), '200': OpenApiResponse(description='Download of file started'),
'201': OpenApiResponse(description='Output backup file is ready for downloading'), '201': OpenApiResponse(description='Output backup file is ready for downloading'),
'202': OpenApiResponse(description='Creating a backup file has been started'), '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') @action(methods=['GET'], detail=True, url_path='backup')
def export_backup(self, request, pk=None): 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) return self.serialize(request, backup.export)
@transaction.atomic @transaction.atomic
......
...@@ -4917,6 +4917,8 @@ paths: ...@@ -4917,6 +4917,8 @@ paths:
description: Output backup file is ready for downloading description: Output backup file is ready for downloading
'202': '202':
description: Creating a backup file has been started description: Creating a backup file has been started
'400':
description: Backup of a task without data is not allowed
/api/tasks/{id}/data/: /api/tasks/{id}/data/:
get: get:
operationId: tasks_retrieve_data operationId: tasks_retrieve_data
......
...@@ -310,6 +310,37 @@ class TestGetProjectBackup: ...@@ -310,6 +310,37 @@ class TestGetProjectBackup:
self._test_can_get_project_backup(user["username"], project["id"]) 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") @pytest.mark.usefixtures("restore_db_per_function")
class TestPostProjects: class TestPostProjects:
...@@ -703,6 +734,37 @@ class TestImportExportDatasetProject: ...@@ -703,6 +734,37 @@ class TestImportExportDatasetProject:
self._test_export_project(username, project_id, "COCO Keypoints 1.0") 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") @pytest.mark.usefixtures("restore_db_per_function")
class TestPatchProjectLabel: class TestPatchProjectLabel:
......
...@@ -21,7 +21,7 @@ from typing import List, Optional ...@@ -21,7 +21,7 @@ from typing import List, Optional
import pytest import pytest
from cvat_sdk import Client, Config, exceptions from cvat_sdk import Client, Config, exceptions
from cvat_sdk.api_client import models 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.helpers import get_paginated_collection
from cvat_sdk.core.proxies.tasks import ResourceType, Task from cvat_sdk.core.proxies.tasks import ResourceType, Task
from cvat_sdk.core.uploading import Uploader from cvat_sdk.core.uploading import Uploader
...@@ -1638,6 +1638,18 @@ class TestTaskBackups: ...@@ -1638,6 +1638,18 @@ class TestTaskBackups:
assert filename.is_file() assert filename.is_file()
assert filename.stat().st_size > 0 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"]) @pytest.mark.parametrize("mode", ["annotation", "interpolation"])
def test_can_import_backup(self, tasks, mode): def test_can_import_backup(self, tasks, mode):
task_json = next(t for t in tasks if t["mode"] == mode) task_json = next(t for t in tasks if t["mode"] == mode)
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册