Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignore tasks without data during project export #6658

Merged
merged 15 commits into from
Aug 24, 2023
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,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>)

Expand Down
6 changes: 5 additions & 1 deletion cvat/apps/dataset_manager/bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,11 @@ def init(self):

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.order_by("subset","id").all()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kirill-sizov , why not .exclude(data=None) as in other places?

if db_task.data is not None
)
)

subsets = set()
Expand Down
4 changes: 2 additions & 2 deletions cvat/apps/dataset_manager/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -98,7 +98,7 @@ def split_name(file):
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)
Expand Down
3 changes: 2 additions & 1 deletion cvat/apps/engine/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,8 @@ def _write_annotation_guide(self, zip_object, target_dir=None):

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():
Expand Down
6 changes: 6 additions & 0 deletions cvat/apps/engine/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -871,9 +871,15 @@ def append_backup_chunk(self, request, file_id):
'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(
zhiltsov-max marked this conversation as resolved.
Show resolved Hide resolved
data='Backup of a task without data is not allowed',
status=status.HTTP_400_BAD_REQUEST
)
return self.serialize(request, backup.export)

@transaction.atomic
Expand Down
2 changes: 2 additions & 0 deletions cvat/schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 62 additions & 0 deletions tests/python/rest_api/test_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,37 @@ def test_org_owner_can_get_project_backup(

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:
Expand Down Expand Up @@ -703,6 +734,37 @@ def test_can_export_dataset_with_skeleton_labels_with_spaces(self):

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:
Expand Down
14 changes: 13 additions & 1 deletion tests/python/rest_api/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
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
Expand Down Expand Up @@ -1638,6 +1638,18 @@ def test_can_export_backup(self, tasks, mode):
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)
Expand Down