From d4edeaaf1bdbc4a078a416ad8ff77ce564367fbd Mon Sep 17 00:00:00 2001 From: allegroai <> Date: Sun, 25 Jul 2021 14:17:29 +0300 Subject: [PATCH] Add projects.validate_delete --- apiserver/bll/project/project_bll.py | 6 +-- apiserver/bll/project/project_cleanup.py | 22 ++++++++ apiserver/schema/services/projects.conf | 44 +++++++++++++-- apiserver/services/projects.py | 13 ++++- .../tests/automated/test_project_delete.py | 54 +++++++++++++++++++ 5 files changed, 131 insertions(+), 8 deletions(-) create mode 100644 apiserver/tests/automated/test_project_delete.py diff --git a/apiserver/bll/project/project_bll.py b/apiserver/bll/project/project_bll.py index 0dcb6b4..4c7ce57 100644 --- a/apiserver/bll/project/project_bll.py +++ b/apiserver/bll/project/project_bll.py @@ -554,7 +554,7 @@ class ProjectBLL: user_ids: Optional[Sequence[str]] = None, ) -> Set[str]: """ - Get the set of user ids that created tasks/models/dataviews in the given projects + Get the set of user ids that created tasks/models in the given projects If project_ids is empty then all projects are examined If user_ids are passed then only subset of these users is returned """ @@ -676,8 +676,8 @@ class ProjectBLL: @classmethod def calc_own_contents(cls, company: str, project_ids: Sequence[str]) -> Dict[str, dict]: """ - Returns the amount of task/dataviews/models per requested project - Use separate aggregation calls on Task/Dataview/Model instead of lookup + Returns the amount of task/models per requested project + Use separate aggregation calls on Task/Model instead of lookup aggregation on projects in order not to hit memory limits on large tasks """ if not project_ids: diff --git a/apiserver/bll/project/project_cleanup.py b/apiserver/bll/project/project_cleanup.py index 782fbab..f7a7afa 100644 --- a/apiserver/bll/project/project_cleanup.py +++ b/apiserver/bll/project/project_cleanup.py @@ -30,6 +30,28 @@ class DeleteProjectResult: urls: TaskUrls = None +def validate_project_delete(company: str, project_id: str): + project = Project.get_for_writing( + company=company, id=project_id, _only=("id", "path") + ) + if not project: + raise errors.bad_request.InvalidProjectId(id=project_id) + + project_ids = _ids_with_children([project_id]) + ret = {} + for cls in (Task, Model): + ret[f"{cls.__name__.lower()}s"] = cls.objects( + project__in=project_ids, + ).count() + for cls in (Task, Model): + ret[f"non_archived_{cls.__name__.lower()}s"] = cls.objects( + project__in=project_ids, + system_tags__nin=[EntityVisibility.archived.value], + ).count() + + return ret + + def delete_project( company: str, project_id: str, force: bool, delete_contents: bool ) -> Tuple[DeleteProjectResult, Set[str]]: diff --git a/apiserver/schema/services/projects.conf b/apiserver/schema/services/projects.conf index 70e1b37..03abea0 100644 --- a/apiserver/schema/services/projects.conf +++ b/apiserver/schema/services/projects.conf @@ -379,7 +379,7 @@ get_all { items { type: string } } page { - description: "Page number, returns a specific page out of the resulting list of dataviews" + description: "Page number, returns a specific page out of the resulting list of projects" type: integer minimum: 0 } @@ -469,7 +469,7 @@ get_all_ex { default: false } check_own_contents { - description: "If set to 'true' and project ids are passed to the query then for these projects their own tasks, models and dataviews are counted" + description: "If set to 'true' and project ids are passed to the query then for these projects their own tasks and models are counted" type: boolean default: false } @@ -594,7 +594,7 @@ merge { type: object properties { moved_entities { - description: "The number of tasks, models and dataviews moved from the merged project into the destination" + description: "The number of tasks and models moved from the merged project into the destination" type: integer } moved_projects { @@ -605,6 +605,42 @@ merge { } } } +validate_delete { + "2.14" { + description: "Validates that the project existis and can be deleted" + request { + type: object + required: [ project ] + properties { + project { + description: "Project ID" + type: string + } + } + } + response { + type: object + properties { + tasks { + description: "The total number of tasks under the project and all its children" + type: integer + } + non_archived_tasks { + description: "The total number of non-archived tasks under the project and all its children" + type: integer + } + models { + description: "The total number of models under the project and all its children" + type: integer + } + non_archived_models { + description: "The total number of non-archived models under the project and all its children" + type: integer + } + } + } + } +} delete { "2.1" { description: "Deletes a project" @@ -613,7 +649,7 @@ delete { required: [ project ] properties { project { - description: "Project id" + description: "Project ID" type: string } force { diff --git a/apiserver/services/projects.py b/apiserver/services/projects.py index e7bdd9a..e861e05 100644 --- a/apiserver/services/projects.py +++ b/apiserver/services/projects.py @@ -16,10 +16,14 @@ from apiserver.apimodels.projects import ( MoveRequest, MergeRequest, ProjectOrNoneRequest, + ProjectRequest, ) from apiserver.bll.organization import OrgBLL, Tags from apiserver.bll.project import ProjectBLL -from apiserver.bll.project.project_cleanup import delete_project +from apiserver.bll.project.project_cleanup import ( + delete_project, + validate_project_delete, +) from apiserver.bll.task import TaskBLL from apiserver.database.errors import translate_errors_context from apiserver.database.model.project import Project @@ -230,6 +234,13 @@ def merge(call: APICall, company: str, request: MergeRequest): } +@endpoint("projects.validate_delete") +def validate_delete(call: APICall, company_id: str, request: ProjectRequest): + call.result.data = validate_project_delete( + company=company_id, project_id=request.project + ) + + @endpoint("projects.delete", request_data_model=DeleteRequest) def delete(call: APICall, company_id: str, request: DeleteRequest): res, affected_projects = delete_project( diff --git a/apiserver/tests/automated/test_project_delete.py b/apiserver/tests/automated/test_project_delete.py new file mode 100644 index 0000000..1bb416c --- /dev/null +++ b/apiserver/tests/automated/test_project_delete.py @@ -0,0 +1,54 @@ +from apiserver.apierrors import errors +from apiserver.database.model import EntityVisibility +from apiserver.tests.automated import TestService +from apiserver.database.utils import id as db_id + + +class TestProjectsDelete(TestService): + def setUp(self, version="2.14"): + super().setUp(version=version) + + def new_task(self, **kwargs): + return self.create_temp( + "tasks", type="testing", name=db_id(), input=dict(view=dict()), **kwargs + ) + + def new_model(self, **kwargs): + return self.create_temp("models", uri="file:///a/b", name=db_id(), labels={}, **kwargs) + + def new_project(self, **kwargs): + return self.create_temp("projects", name=db_id(), description="", **kwargs) + + def test_delete_fails_with_active_task(self): + project = self.new_project() + self.new_task(project=project) + res = self.api.projects.validate_delete(project=project) + self.assertEqual(res.tasks, 1) + self.assertEqual(res.non_archived_tasks, 1) + with self.api.raises(errors.bad_request.ProjectHasTasks): + self.api.projects.delete(project=project) + + def test_delete_with_archived_task(self): + project = self.new_project() + self.new_task(project=project, system_tags=[EntityVisibility.archived.value]) + res = self.api.projects.validate_delete(project=project) + self.assertEqual(res.tasks, 1) + self.assertEqual(res.non_archived_tasks, 0) + self.api.projects.delete(project=project) + + def test_delete_fails_with_active_model(self): + project = self.new_project() + self.new_model(project=project) + res = self.api.projects.validate_delete(project=project) + self.assertEqual(res.models, 1) + self.assertEqual(res.non_archived_models, 1) + with self.api.raises(errors.bad_request.ProjectHasModels): + self.api.projects.delete(project=project) + + def test_delete_with_archived_model(self): + project = self.new_project() + self.new_model(project=project, system_tags=[EntityVisibility.archived.value]) + res = self.api.projects.validate_delete(project=project) + self.assertEqual(res.models, 1) + self.assertEqual(res.non_archived_models, 0) + self.api.projects.delete(project=project)