1
1
import asyncio
2
+ import datetime
2
3
import logging
3
4
4
5
import arrow
5
6
from aiohttp import web
7
+ from common_library .pagination_tools import iter_pagination_params
8
+ from models_library .basic_types import IDStr
6
9
from models_library .products import ProductName
7
10
from models_library .projects import ProjectID
11
+ from models_library .rest_ordering import OrderBy , OrderDirection
8
12
from models_library .users import UserID
9
13
from servicelib .aiohttp .application_keys import APP_FIRE_AND_FORGET_TASKS_KEY
10
14
from servicelib .common_headers import UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE
11
15
from servicelib .utils import fire_and_forget_task
12
16
13
17
from ..director_v2 import api as director_v2_api
14
18
from ..dynamic_scheduler import api as dynamic_scheduler_api
15
- from . import projects_service
19
+ from . import _crud_api_read , projects_service
16
20
from ._access_rights_api import check_user_project_permission
17
- from .exceptions import ProjectRunningConflictError
18
- from .models import ProjectPatchInternalExtended
21
+ from .exceptions import (
22
+ ProjectNotFoundError ,
23
+ ProjectNotTrashedError ,
24
+ ProjectRunningConflictError ,
25
+ )
26
+ from .models import ProjectDict , ProjectPatchInternalExtended
19
27
20
28
_logger = logging .getLogger (__name__ )
21
29
@@ -45,7 +53,7 @@ async def trash_project(
45
53
project_id : ProjectID ,
46
54
force_stop_first : bool ,
47
55
explicit : bool ,
48
- ):
56
+ ) -> None :
49
57
"""
50
58
51
59
Raises:
@@ -108,7 +116,7 @@ async def untrash_project(
108
116
product_name : ProductName ,
109
117
user_id : UserID ,
110
118
project_id : ProjectID ,
111
- ):
119
+ ) -> None :
112
120
# NOTE: check_user_project_permission is inside projects_api.patch_project
113
121
await projects_service .patch_project (
114
122
app ,
@@ -119,3 +127,110 @@ async def untrash_project(
119
127
trashed_at = None , trashed_explicitly = False , trashed_by = None
120
128
),
121
129
)
130
+
131
+
132
+ def _can_delete (
133
+ project : ProjectDict ,
134
+ user_id : UserID ,
135
+ until_equal_datetime : datetime .datetime | None ,
136
+ ) -> bool :
137
+ """
138
+ This is the current policy to delete trashed project
139
+
140
+ """
141
+ trashed_at = project .get ("trashed" )
142
+ trashed_by = project .get ("trashedBy" )
143
+ trashed_explicitly = project .get ("trashedExplicitly" )
144
+
145
+ assert trashed_at is not None # nosec
146
+ assert trashed_by is not None # nosec
147
+
148
+ is_shared = len (project ["accessRights" ]) > 1
149
+
150
+ return bool (
151
+ trashed_at
152
+ and (until_equal_datetime is None or trashed_at < until_equal_datetime )
153
+ # NOTE: current policy is more restricted until
154
+ # logic is adapted to deal with the other cases
155
+ and trashed_by == user_id
156
+ and not is_shared
157
+ and trashed_explicitly
158
+ )
159
+
160
+
161
+ async def list_trashed_projects (
162
+ app : web .Application ,
163
+ * ,
164
+ product_name : ProductName ,
165
+ user_id : UserID ,
166
+ until_equal_datetime : datetime .datetime | None = None ,
167
+ ) -> list [ProjectID ]:
168
+ """
169
+ Lists all projects that were trashed until a specific datetime (if !=None).
170
+ """
171
+ trashed_projects : list [ProjectID ] = []
172
+
173
+ for page_params in iter_pagination_params (limit = 100 ):
174
+ (
175
+ projects ,
176
+ page_params .total_number_of_items ,
177
+ ) = await _crud_api_read .list_projects_full_depth (
178
+ app ,
179
+ user_id = user_id ,
180
+ product_name = product_name ,
181
+ trashed = True ,
182
+ tag_ids_list = [],
183
+ offset = page_params .offset ,
184
+ limit = page_params .limit ,
185
+ order_by = OrderBy (field = IDStr ("trashed" ), direction = OrderDirection .ASC ),
186
+ search_by_multi_columns = None ,
187
+ search_by_project_name = None ,
188
+ )
189
+
190
+ # NOTE: Applying POST-FILTERING because we do not want to modify the interface of
191
+ # _crud_api_read.list_projects_full_depth at this time.
192
+ # This filtering couldn't be handled at the database level when `projects_repo`
193
+ # was refactored, as defining a custom trash_filter was needed to allow more
194
+ # flexibility in filtering options.
195
+ trashed_projects = [
196
+ project ["uuid" ]
197
+ for project in projects
198
+ if _can_delete (project , user_id , until_equal_datetime )
199
+ ]
200
+ return trashed_projects
201
+
202
+
203
+ async def delete_trashed_project (
204
+ app : web .Application ,
205
+ * ,
206
+ user_id : UserID ,
207
+ project_id : ProjectID ,
208
+ until_equal_datetime : datetime .datetime | None = None ,
209
+ ) -> None :
210
+ """
211
+ Deletes a project that was explicitly trashed by the user from a specific datetime (if provided, otherwise all).
212
+
213
+ Raises:
214
+ ProjectNotFoundError: If the project is not found.
215
+ ProjectNotTrashedError: If the project was not trashed explicitly by the user from the specified datetime.
216
+ """
217
+ project = await projects_service .get_project_for_user (
218
+ app , project_uuid = f"{ project_id } " , user_id = user_id
219
+ )
220
+
221
+ if not project :
222
+ raise ProjectNotFoundError (project_uuid = project_id , user_id = user_id )
223
+
224
+ if not _can_delete (project , user_id , until_equal_datetime ):
225
+ # safety check
226
+ raise ProjectNotTrashedError (
227
+ project_uuid = project_id ,
228
+ user_id = user_id ,
229
+ reason = "Cannot delete trashed project since it does not fit current criteria" ,
230
+ )
231
+
232
+ await projects_service .delete_project_by_user (
233
+ app ,
234
+ user_id = user_id ,
235
+ project_uuid = project_id ,
236
+ )
0 commit comments