Skip to content

Commit a334d69

Browse files
committed
drafts project trash deletiong
1 parent 8edfd52 commit a334d69

File tree

4 files changed

+119
-5
lines changed

4 files changed

+119
-5
lines changed

services/web/server/src/simcore_service_webserver/projects/_trash_service.py

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
import asyncio
2+
import datetime
23
import logging
34

45
import arrow
56
from aiohttp import web
7+
from models_library.basic_types import IDStr
68
from models_library.products import ProductName
79
from models_library.projects import ProjectID
10+
from models_library.rest_ordering import OrderBy, OrderDirection
811
from models_library.users import UserID
912
from servicelib.aiohttp.application_keys import APP_FIRE_AND_FORGET_TASKS_KEY
1013
from servicelib.common_headers import UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE
1114
from servicelib.utils import fire_and_forget_task
1215

1316
from ..director_v2 import api as director_v2_api
1417
from ..dynamic_scheduler import api as dynamic_scheduler_api
15-
from . import projects_service
18+
from . import _crud_api_read, projects_service
1619
from ._access_rights_api import check_user_project_permission
17-
from .exceptions import ProjectRunningConflictError
20+
from .exceptions import (
21+
ProjectNotFoundError,
22+
ProjectNotTrashedError,
23+
ProjectRunningConflictError,
24+
)
1825
from .models import ProjectPatchInternalExtended
1926

2027
_logger = logging.getLogger(__name__)
@@ -45,7 +52,7 @@ async def trash_project(
4552
project_id: ProjectID,
4653
force_stop_first: bool,
4754
explicit: bool,
48-
):
55+
) -> None:
4956
"""
5057
5158
Raises:
@@ -108,7 +115,7 @@ async def untrash_project(
108115
product_name: ProductName,
109116
user_id: UserID,
110117
project_id: ProjectID,
111-
):
118+
) -> None:
112119
# NOTE: check_user_project_permission is inside projects_api.patch_project
113120
await projects_service.patch_project(
114121
app,
@@ -119,3 +126,90 @@ async def untrash_project(
119126
trashed_at=None, trashed_explicitly=False, trashed_by=None
120127
),
121128
)
129+
130+
131+
async def list_trashed_projects(
132+
app: web.Application,
133+
*,
134+
product_name: ProductName,
135+
user_id: UserID,
136+
until_equal_datetime: datetime.datetime | None = None,
137+
) -> list[ProjectID]:
138+
"""
139+
Lists all projects that were trashed until a specific datetime.
140+
"""
141+
projects, _ = await _crud_api_read.list_projects_full_depth(
142+
request=app,
143+
user_id=user_id,
144+
product_name=product_name,
145+
trashed=True,
146+
tag_ids_list=[],
147+
offset=0,
148+
limit=100, # FIXME: this is only the first 100. redo with yield
149+
order_by=OrderBy(field=IDStr("trashed"), direction=OrderDirection.ASC),
150+
search_by_multi_columns=None,
151+
search_by_project_name=None,
152+
)
153+
154+
# NOTE; this can be done at the database level when projects_repo is refactored
155+
# by defining a custom trash_filter that permits some flexibility in the filtering options
156+
trashed_projects = []
157+
for project in projects:
158+
trashed_at = project.get("trashed_at")
159+
trashed_by = project.get("trashed_by")
160+
trashed_explicitly = project.get("trashed_explicitly")
161+
162+
if (
163+
trashed_at
164+
and (until_equal_datetime is None or trashed_at < until_equal_datetime)
165+
and trashed_by == user_id
166+
and trashed_explicitly
167+
):
168+
trashed_projects.append(project)
169+
170+
return [project["uuid"] for project in trashed_projects]
171+
172+
173+
async def delete_trashed_project(
174+
app: web.Application,
175+
*,
176+
user_id: UserID,
177+
project_id: ProjectID,
178+
until_equal_datetime: datetime.datetime | None = None,
179+
) -> None:
180+
"""
181+
Deletes a project that was explicitly trashed by the user from a specific datetime (if provided, otherwise all).
182+
183+
Raises:
184+
ProjectNotFoundError: If the project is not found.
185+
ProjectNotTrashedError: If the project was not trashed explicitly by the user from the specified datetime.
186+
"""
187+
project = await projects_service.get_project_for_user(
188+
app, project_uuid=f"{project_id}", user_id=user_id
189+
)
190+
191+
if not project:
192+
raise ProjectNotFoundError(project_uuid=project_id, user_id=user_id)
193+
194+
trashed_at = project.get("trashed_at")
195+
trashed_by = project.get("trashed_by")
196+
trashed_explicitly = project.get("trashed_explicitly")
197+
198+
if (
199+
not trashed_at
200+
or (until_equal_datetime is not None and until_equal_datetime < trashed_at)
201+
or trashed_by != user_id
202+
or not trashed_explicitly
203+
):
204+
raise ProjectNotTrashedError(
205+
project_uuid=project_id,
206+
user_id=user_id,
207+
reason="Project was not trashed explicitly by the user from the specified datetime",
208+
)
209+
# FIXME: locking while deleting?
210+
# FIXME: this can be heavy. Rather call delete_trashed_project as a task
211+
await projects_service.delete_project_by_user(
212+
app,
213+
user_id=user_id,
214+
project_uuid=project_id,
215+
)

services/web/server/src/simcore_service_webserver/projects/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ class ProjectRunningConflictError(ProjectTrashError):
9393
)
9494

9595

96+
class ProjectNotTrashedError(ProjectTrashError):
97+
msg_template = (
98+
"Cannot delete project {project_uuid} since it was not trashed first: {reason}"
99+
)
100+
101+
96102
class NodeNotFoundError(BaseProjectError):
97103
msg_template = "Node '{node_uuid}' not found in project '{project_uuid}'"
98104

services/web/server/src/simcore_service_webserver/trash/_service.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from models_library.products import ProductName
77
from models_library.users import UserID
88

9+
from ..projects import _trash_service
910
from .settings import get_plugin_settings
1011

1112
_logger = logging.getLogger(__name__)
@@ -19,7 +20,16 @@ async def empty_trash(app: web.Application, product_name: ProductName, user_id:
1920
f"{user_id=}",
2021
f"{product_name=}",
2122
)
22-
raise NotImplementedError
23+
24+
trashed_projects_ids = await _trash_service.list_trashed_projects(
25+
app=app, product_name=product_name, user_id=user_id
26+
)
27+
28+
for project_id in trashed_projects_ids:
29+
# TODO: handle error. should not be fail-fast!
30+
await _trash_service.delete_trashed_project(
31+
app, user_id=user_id, project_id=project_id
32+
)
2333

2434

2535
async def prune_trash(app: web.Application) -> list[str]:

services/web/server/tests/unit/with_dbs/03/test_trash.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,3 +841,7 @@ async def test_trash_project_explitictly_and_empty_trash_bin(
841841
await assert_status(resp, status.HTTP_200_OK)
842842
page = Page[ProjectListItem].model_validate(await resp.json())
843843
assert page.meta.total == 0
844+
845+
# GET trahsed project
846+
resp = await client.get(f"/v0/projects/{project_uuid}")
847+
await assert_status(resp, status.HTTP_404_NOT_FOUND)

0 commit comments

Comments
 (0)