未验证 提交 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
### 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>)
- 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
......
......@@ -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()
......
......@@ -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)
......
......@@ -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():
......
......@@ -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
......
......@@ -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
......
......@@ -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:
......
......@@ -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)
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册