Add projects.validate_delete

This commit is contained in:
allegroai 2021-07-25 14:17:29 +03:00
parent 56aea1ffb8
commit d4edeaaf1b
5 changed files with 131 additions and 8 deletions

View File

@ -554,7 +554,7 @@ class ProjectBLL:
user_ids: Optional[Sequence[str]] = None, user_ids: Optional[Sequence[str]] = None,
) -> Set[str]: ) -> 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 project_ids is empty then all projects are examined
If user_ids are passed then only subset of these users is returned If user_ids are passed then only subset of these users is returned
""" """
@ -676,8 +676,8 @@ class ProjectBLL:
@classmethod @classmethod
def calc_own_contents(cls, company: str, project_ids: Sequence[str]) -> Dict[str, dict]: def calc_own_contents(cls, company: str, project_ids: Sequence[str]) -> Dict[str, dict]:
""" """
Returns the amount of task/dataviews/models per requested project Returns the amount of task/models per requested project
Use separate aggregation calls on Task/Dataview/Model instead of lookup Use separate aggregation calls on Task/Model instead of lookup
aggregation on projects in order not to hit memory limits on large tasks aggregation on projects in order not to hit memory limits on large tasks
""" """
if not project_ids: if not project_ids:

View File

@ -30,6 +30,28 @@ class DeleteProjectResult:
urls: TaskUrls = None 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( def delete_project(
company: str, project_id: str, force: bool, delete_contents: bool company: str, project_id: str, force: bool, delete_contents: bool
) -> Tuple[DeleteProjectResult, Set[str]]: ) -> Tuple[DeleteProjectResult, Set[str]]:

View File

@ -379,7 +379,7 @@ get_all {
items { type: string } items { type: string }
} }
page { 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 type: integer
minimum: 0 minimum: 0
} }
@ -469,7 +469,7 @@ get_all_ex {
default: false default: false
} }
check_own_contents { 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 type: boolean
default: false default: false
} }
@ -594,7 +594,7 @@ merge {
type: object type: object
properties { properties {
moved_entities { 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 type: integer
} }
moved_projects { 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 { delete {
"2.1" { "2.1" {
description: "Deletes a project" description: "Deletes a project"
@ -613,7 +649,7 @@ delete {
required: [ project ] required: [ project ]
properties { properties {
project { project {
description: "Project id" description: "Project ID"
type: string type: string
} }
force { force {

View File

@ -16,10 +16,14 @@ from apiserver.apimodels.projects import (
MoveRequest, MoveRequest,
MergeRequest, MergeRequest,
ProjectOrNoneRequest, ProjectOrNoneRequest,
ProjectRequest,
) )
from apiserver.bll.organization import OrgBLL, Tags from apiserver.bll.organization import OrgBLL, Tags
from apiserver.bll.project import ProjectBLL 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.bll.task import TaskBLL
from apiserver.database.errors import translate_errors_context from apiserver.database.errors import translate_errors_context
from apiserver.database.model.project import Project 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) @endpoint("projects.delete", request_data_model=DeleteRequest)
def delete(call: APICall, company_id: str, request: DeleteRequest): def delete(call: APICall, company_id: str, request: DeleteRequest):
res, affected_projects = delete_project( res, affected_projects = delete_project(

View File

@ -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)