Skip to content

Commit 925b817

Browse files
matusdrobuliak66mrnicegyu11
authored andcommitted
🎨 adding folder_id to project resource (ITISFoundation#6460)
1 parent 3200fe2 commit 925b817

File tree

16 files changed

+149
-22
lines changed

16 files changed

+149
-22
lines changed

‎packages/models-library/src/models_library/api_schemas_webserver/projects.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class ProjectGet(OutputSchema):
8484
dev: dict | None
8585
permalink: ProjectPermalink = FieldNotRequired()
8686
workspace_id: WorkspaceID | None
87+
folder_id: FolderID | None
8788

8889
_empty_description = validator("description", allow_reuse=True, pre=True)(
8990
none_to_empty_str_pre_validator

‎packages/models-library/src/models_library/projects.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Any, Final, TypeAlias
99
from uuid import UUID
1010

11+
from models_library.folders import FolderID
1112
from models_library.workspaces import WorkspaceID
1213
from pydantic import BaseModel, ConstrainedStr, Extra, Field, validator
1314

@@ -179,6 +180,11 @@ class Project(BaseProjectModel):
179180
description="To which workspace project belongs. If None, belongs to private user workspace.",
180181
alias="workspaceId",
181182
)
183+
folder_id: FolderID | None = Field(
184+
default=None,
185+
description="To which folder project belongs. If None, belongs to root folder.",
186+
alias="folderId",
187+
)
182188

183189
class Config:
184190
description = "Document that stores metadata, pipeline and UI setup of a study"

‎services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10236,6 +10236,11 @@ components:
1023610236
exclusiveMinimum: true
1023710237
type: integer
1023810238
minimum: 0
10239+
folderId:
10240+
title: Folderid
10241+
exclusiveMinimum: true
10242+
type: integer
10243+
minimum: 0
1023910244
ProjectGroupGet:
1024010245
title: ProjectGroupGet
1024110246
required:
@@ -10471,6 +10476,11 @@ components:
1047110476
exclusiveMinimum: true
1047210477
type: integer
1047310478
minimum: 0
10479+
folderId:
10480+
title: Folderid
10481+
exclusiveMinimum: true
10482+
type: integer
10483+
minimum: 0
1047410484
ProjectLocked:
1047510485
title: ProjectLocked
1047610486
required:

‎services/web/server/src/simcore_service_webserver/projects/_access_rights_api.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from ._access_rights_db import get_project_owner
1010
from .db import APP_PROJECT_DBAPI, ProjectDBAPI
1111
from .exceptions import ProjectInvalidRightsError, ProjectNotFoundError
12-
from .models import UserProjectAccessRights
12+
from .models import UserProjectAccessRightsWithWorkspace
1313

1414

1515
async def validate_project_ownership(
@@ -31,7 +31,7 @@ async def get_user_project_access_rights(
3131
project_id: ProjectID,
3232
user_id: UserID,
3333
product_name: ProductName,
34-
) -> UserProjectAccessRights:
34+
) -> UserProjectAccessRightsWithWorkspace:
3535
"""
3636
This function resolves user access rights on the project resource.
3737
@@ -51,19 +51,31 @@ async def get_user_project_access_rights(
5151
workspace_id=project_db.workspace_id,
5252
product_name=product_name,
5353
)
54-
_user_project_access_rights = UserProjectAccessRights(
55-
uid=user_id,
56-
read=workspace.my_access_rights.read,
57-
write=workspace.my_access_rights.write,
58-
delete=workspace.my_access_rights.delete,
54+
_user_project_access_rights_with_workspace = (
55+
UserProjectAccessRightsWithWorkspace(
56+
uid=user_id,
57+
workspace_id=project_db.workspace_id,
58+
read=workspace.my_access_rights.read,
59+
write=workspace.my_access_rights.write,
60+
delete=workspace.my_access_rights.delete,
61+
)
5962
)
6063
else:
6164
_user_project_access_rights = (
6265
await db.get_pure_project_access_rights_without_workspace(
6366
user_id, project_id
6467
)
6568
)
66-
return _user_project_access_rights
69+
_user_project_access_rights_with_workspace = (
70+
UserProjectAccessRightsWithWorkspace(
71+
uid=user_id,
72+
workspace_id=None,
73+
read=_user_project_access_rights.read,
74+
write=_user_project_access_rights.write,
75+
delete=_user_project_access_rights.delete,
76+
)
77+
)
78+
return _user_project_access_rights_with_workspace
6779

6880

6981
async def has_user_project_access_rights(
@@ -92,7 +104,7 @@ async def check_user_project_permission(
92104
user_id: UserID,
93105
product_name: ProductName,
94106
permission: PermissionStr = "read",
95-
) -> UserProjectAccessRights:
107+
) -> UserProjectAccessRightsWithWorkspace:
96108
_user_project_access_rights = await get_user_project_access_rights(
97109
app, project_id=project_id, user_id=user_id, product_name=product_name
98110
)

‎services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,13 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche
398398
# Adds permalink
399399
await update_or_pop_permalink_in_project(request, new_project)
400400

401+
# Adds folderId
402+
user_specific_project_data_db = await db.get_user_specific_project_data_db(
403+
project_uuid=new_project["uuid"],
404+
private_workspace_user_id_or_none=user_id if workspace_id is None else None,
405+
)
406+
new_project["folderId"] = user_specific_project_data_db.folder_id
407+
401408
# Overwrite project access rights
402409
if workspace_id:
403410
workspace_db: UserWorkspaceAccessRightsDB = (

‎services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ async def replace_project(request: web.Request):
444444
reason=f"Project {path_params.project_id} cannot be modified while pipeline is still running."
445445
)
446446

447-
await check_user_project_permission(
447+
user_project_permission = await check_user_project_permission(
448448
request.app,
449449
project_id=path_params.project_id,
450450
user_id=req_ctx.user_id,
@@ -483,6 +483,16 @@ async def replace_project(request: web.Request):
483483
is_template=False,
484484
app=request.app,
485485
)
486+
# Appends folder ID
487+
user_specific_project_data_db = await db.get_user_specific_project_data_db(
488+
project_uuid=path_params.project_id,
489+
private_workspace_user_id_or_none=(
490+
req_ctx.user_id
491+
if user_project_permission.workspace_id is None
492+
else None
493+
),
494+
)
495+
data["folderId"] = user_specific_project_data_db.folder_id
486496

487497
return web.json_response({"data": data}, dumps=json_dumps)
488498

‎services/web/server/src/simcore_service_webserver/projects/_db_utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def convert_to_db_names(project_document_data: dict) -> dict:
6464
exclude_keys = [
6565
"tags",
6666
"prjOwner",
67+
"folderId",
6768
] # No column for tags, prjOwner is a foreign key in db
6869
for key, value in project_document_data.items():
6970
if key not in exclude_keys:

‎services/web/server/src/simcore_service_webserver/projects/db.py

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,12 @@
8787
ProjectNodeResourcesInsufficientRightsError,
8888
ProjectNotFoundError,
8989
)
90-
from .models import ProjectDB, ProjectDict, UserProjectAccessRights
90+
from .models import (
91+
ProjectDB,
92+
ProjectDict,
93+
UserProjectAccessRightsDB,
94+
UserSpecificProjectDataDB,
95+
)
9196

9297
_logger = logging.getLogger(__name__)
9398

@@ -398,6 +403,7 @@ async def list_projects( # pylint: disable=too-many-arguments
398403
],
399404
access_rights_subquery.c.access_rights,
400405
projects_to_products.c.product_name,
406+
projects_to_folders.c.folder_id,
401407
)
402408
.select_from(_join_query)
403409
.where(
@@ -496,13 +502,9 @@ async def get_project(
496502
only_published: bool = False,
497503
only_templates: bool = False,
498504
) -> tuple[ProjectDict, ProjectType]:
499-
"""Returns all projects *owned* by the user
500-
501-
- prj_owner
502-
- Notice that a user can have access to a template but he might not own it
503-
- Notice that a user can have access to a project where he/she has read access
504-
505-
:raises ProjectNotFoundError: project is not assigned to user
505+
"""
506+
This is a legacy function that retrieves the project resource along with additional adjustments.
507+
The `get_project_db` function is now recommended for use when interacting with the projects DB layer.
506508
"""
507509
async with self.engine.acquire() as conn:
508510
project = await self._get_project(
@@ -553,9 +555,37 @@ async def get_project_db(self, project_uuid: ProjectID) -> ProjectDB:
553555
raise ProjectNotFoundError(project_uuid=project_uuid)
554556
return ProjectDB.from_orm(row)
555557

558+
async def get_user_specific_project_data_db(
559+
self, project_uuid: ProjectID, private_workspace_user_id_or_none: UserID | None
560+
) -> UserSpecificProjectDataDB:
561+
async with self.engine.acquire() as conn:
562+
result = await conn.execute(
563+
sa.select(
564+
*self._SELECTION_PROJECT_DB_ARGS, projects_to_folders.c.folder_id
565+
)
566+
.select_from(
567+
projects.join(
568+
projects_to_folders,
569+
(
570+
(projects_to_folders.c.project_uuid == projects.c.uuid)
571+
& (
572+
projects_to_folders.c.user_id
573+
== private_workspace_user_id_or_none
574+
)
575+
),
576+
isouter=True,
577+
)
578+
)
579+
.where(projects.c.uuid == f"{project_uuid}")
580+
)
581+
row = await result.fetchone()
582+
if row is None:
583+
raise ProjectNotFoundError(project_uuid=project_uuid)
584+
return UserSpecificProjectDataDB.from_orm(row)
585+
556586
async def get_pure_project_access_rights_without_workspace(
557587
self, user_id: UserID, project_uuid: ProjectID
558-
) -> UserProjectAccessRights:
588+
) -> UserProjectAccessRightsDB:
559589
"""
560590
Be careful what you want. You should use `get_user_project_access_rights` to get access rights on the
561591
project. It depends on which context you are in, whether private or shared workspace.
@@ -597,7 +627,7 @@ async def get_pure_project_access_rights_without_workspace(
597627
raise ProjectInvalidRightsError(
598628
user_id=user_id, project_uuid=project_uuid
599629
)
600-
return UserProjectAccessRights.from_orm(row)
630+
return UserProjectAccessRightsDB.from_orm(row)
601631

602632
async def replace_project(
603633
self,

‎services/web/server/src/simcore_service_webserver/projects/models.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from aiopg.sa.result import RowProxy
66
from models_library.basic_types import HttpUrlWithCustomMinLength
7+
from models_library.folders import FolderID
78
from models_library.projects import ClassifierID, ProjectID
89
from models_library.projects_ui import StudyUI
910
from models_library.users import UserID
@@ -63,13 +64,31 @@ class Config:
6364
)
6465

6566

67+
class UserSpecificProjectDataDB(ProjectDB):
68+
folder_id: FolderID | None
69+
70+
class Config:
71+
orm_mode = True
72+
73+
6674
assert set(ProjectDB.__fields__.keys()).issubset( # nosec
6775
{c.name for c in projects.columns if c.name not in ["access_rights"]}
6876
)
6977

7078

71-
class UserProjectAccessRights(BaseModel):
79+
class UserProjectAccessRightsDB(BaseModel):
80+
uid: UserID
81+
read: bool
82+
write: bool
83+
delete: bool
84+
85+
class Config:
86+
orm_mode = True
87+
88+
89+
class UserProjectAccessRightsWithWorkspace(BaseModel):
7290
uid: UserID
91+
workspace_id: WorkspaceID | None # None if it's a private workspace
7392
read: bool
7493
write: bool
7594
delete: bool

‎services/web/server/src/simcore_service_webserver/projects/projects_api.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,18 +180,26 @@ async def get_project_for_user(
180180
db = ProjectDBAPI.get_from_app_context(app)
181181

182182
product_name = await db.get_project_product(ProjectID(project_uuid))
183-
await check_user_project_permission(
183+
user_project_access = await check_user_project_permission(
184184
app,
185185
project_id=ProjectID(project_uuid),
186186
user_id=user_id,
187187
product_name=product_name,
188188
permission=cast(PermissionStr, check_permissions),
189189
)
190+
workspace_is_private = user_project_access.workspace_id is None
190191

191192
project, project_type = await db.get_project(
192193
project_uuid,
193194
)
194195

196+
# add folder id to the project base on the user
197+
user_specific_project_data_db = await db.get_user_specific_project_data_db(
198+
project_uuid=ProjectID(project_uuid),
199+
private_workspace_user_id_or_none=user_id if workspace_is_private else None,
200+
)
201+
project["folderId"] = user_specific_project_data_db.folder_id
202+
195203
# adds state if it is not a template
196204
if include_state:
197205
project = await add_project_states_for_user(

‎services/web/server/tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ async def _creator(
423423
# dynamic
424424
"state",
425425
"permalink",
426+
"folderId",
426427
]
427428

428429
for key in new_project:

‎services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ async def _assert_get_same_project(
168168
# Optional fields are not part of reference 'project'
169169
project_state = data.pop("state")
170170
project_permalink = data.pop("permalink", None)
171+
folder_id = data.pop("folderId", None)
171172

172173
assert data == project
173174

@@ -177,6 +178,8 @@ async def _assert_get_same_project(
177178
if project_permalink:
178179
assert parse_obj_as(ProjectPermalink, project_permalink)
179180

181+
assert folder_id is None
182+
180183

181184
async def _replace_project(
182185
client: TestClient, project_update: dict, expected: HTTPStatus
@@ -222,6 +225,7 @@ async def test_list_projects(
222225
# template project
223226
project_state = data[0].pop("state")
224227
project_permalink = data[0].pop("permalink")
228+
folder_id = data[0].pop("folderId")
225229

226230
assert data[0] == template_project
227231
assert not ProjectState(
@@ -232,10 +236,12 @@ async def test_list_projects(
232236
# standard project
233237
project_state = data[1].pop("state")
234238
project_permalink = data[1].pop("permalink", None)
239+
folder_id = data[1].pop("folderId")
235240

236241
assert data[1] == user_project
237242
assert ProjectState(**project_state)
238243
assert project_permalink is None
244+
assert folder_id is None
239245

240246
# GET /v0/projects?type=user
241247
data, *_ = await _list_and_assert_projects(client, expected, {"type": "user"})
@@ -245,6 +251,7 @@ async def test_list_projects(
245251
# standad project
246252
project_state = data[0].pop("state")
247253
project_permalink = data[0].pop("permalink", None)
254+
folder_id = data[0].pop("folderId")
248255

249256
assert data[0] == user_project
250257
assert not ProjectState(
@@ -261,6 +268,7 @@ async def test_list_projects(
261268
# template project
262269
project_state = data[0].pop("state")
263270
project_permalink = data[0].pop("permalink")
271+
folder_id = data[0].pop("folderId")
264272

265273
assert data[0] == template_project
266274
assert not ProjectState(

‎services/web/server/tests/unit/with_dbs/02/test_projects_states_handlers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,7 @@ async def test_get_active_project(
920920
)
921921
assert not error
922922
assert ProjectState(**data.pop("state")).locked.value
923+
data.pop("folderId")
923924

924925
user_project_last_change_date = user_project.pop("lastChangeDate")
925926
data_last_change_date = data.pop("lastChangeDate")
@@ -1416,6 +1417,7 @@ async def test_open_shared_project_at_same_time(
14161417
num_assertions += 1
14171418
elif data:
14181419
project_status = ProjectState(**data.pop("state"))
1420+
data.pop("folderId")
14191421
assert data == shared_project
14201422
assert project_status.locked.value
14211423
assert project_status.locked.owner

0 commit comments

Comments
 (0)