Skip to content

Commit

Permalink
Merge branch 'master' into is1701/tip-lite-tutorial-minor
Browse files Browse the repository at this point in the history
  • Loading branch information
pcrespov authored Sep 27, 2024
2 parents e7b85d0 + a50d80c commit ec575af
Show file tree
Hide file tree
Showing 16 changed files with 149 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class ProjectGet(OutputSchema):
dev: dict | None
permalink: ProjectPermalink = FieldNotRequired()
workspace_id: WorkspaceID | None
folder_id: FolderID | None

_empty_description = validator("description", allow_reuse=True, pre=True)(
none_to_empty_str_pre_validator
Expand Down
6 changes: 6 additions & 0 deletions packages/models-library/src/models_library/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing import Any, Final, TypeAlias
from uuid import UUID

from models_library.folders import FolderID
from models_library.workspaces import WorkspaceID
from pydantic import BaseModel, ConstrainedStr, Extra, Field, validator

Expand Down Expand Up @@ -179,6 +180,11 @@ class Project(BaseProjectModel):
description="To which workspace project belongs. If None, belongs to private user workspace.",
alias="workspaceId",
)
folder_id: FolderID | None = Field(
default=None,
description="To which folder project belongs. If None, belongs to root folder.",
alias="folderId",
)

class Config:
description = "Document that stores metadata, pipeline and UI setup of a study"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10236,6 +10236,11 @@ components:
exclusiveMinimum: true
type: integer
minimum: 0
folderId:
title: Folderid
exclusiveMinimum: true
type: integer
minimum: 0
ProjectGroupGet:
title: ProjectGroupGet
required:
Expand Down Expand Up @@ -10471,6 +10476,11 @@ components:
exclusiveMinimum: true
type: integer
minimum: 0
folderId:
title: Folderid
exclusiveMinimum: true
type: integer
minimum: 0
ProjectLocked:
title: ProjectLocked
required:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from ._access_rights_db import get_project_owner
from .db import APP_PROJECT_DBAPI, ProjectDBAPI
from .exceptions import ProjectInvalidRightsError, ProjectNotFoundError
from .models import UserProjectAccessRights
from .models import UserProjectAccessRightsWithWorkspace


async def validate_project_ownership(
Expand All @@ -31,7 +31,7 @@ async def get_user_project_access_rights(
project_id: ProjectID,
user_id: UserID,
product_name: ProductName,
) -> UserProjectAccessRights:
) -> UserProjectAccessRightsWithWorkspace:
"""
This function resolves user access rights on the project resource.
Expand All @@ -51,19 +51,31 @@ async def get_user_project_access_rights(
workspace_id=project_db.workspace_id,
product_name=product_name,
)
_user_project_access_rights = UserProjectAccessRights(
uid=user_id,
read=workspace.my_access_rights.read,
write=workspace.my_access_rights.write,
delete=workspace.my_access_rights.delete,
_user_project_access_rights_with_workspace = (
UserProjectAccessRightsWithWorkspace(
uid=user_id,
workspace_id=project_db.workspace_id,
read=workspace.my_access_rights.read,
write=workspace.my_access_rights.write,
delete=workspace.my_access_rights.delete,
)
)
else:
_user_project_access_rights = (
await db.get_pure_project_access_rights_without_workspace(
user_id, project_id
)
)
return _user_project_access_rights
_user_project_access_rights_with_workspace = (
UserProjectAccessRightsWithWorkspace(
uid=user_id,
workspace_id=None,
read=_user_project_access_rights.read,
write=_user_project_access_rights.write,
delete=_user_project_access_rights.delete,
)
)
return _user_project_access_rights_with_workspace


async def has_user_project_access_rights(
Expand Down Expand Up @@ -92,7 +104,7 @@ async def check_user_project_permission(
user_id: UserID,
product_name: ProductName,
permission: PermissionStr = "read",
) -> UserProjectAccessRights:
) -> UserProjectAccessRightsWithWorkspace:
_user_project_access_rights = await get_user_project_access_rights(
app, project_id=project_id, user_id=user_id, product_name=product_name
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,13 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche
# Adds permalink
await update_or_pop_permalink_in_project(request, new_project)

# Adds folderId
user_specific_project_data_db = await db.get_user_specific_project_data_db(
project_uuid=new_project["uuid"],
private_workspace_user_id_or_none=user_id if workspace_id is None else None,
)
new_project["folderId"] = user_specific_project_data_db.folder_id

# Overwrite project access rights
if workspace_id:
workspace_db: UserWorkspaceAccessRightsDB = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ async def replace_project(request: web.Request):
reason=f"Project {path_params.project_id} cannot be modified while pipeline is still running."
)

await check_user_project_permission(
user_project_permission = await check_user_project_permission(
request.app,
project_id=path_params.project_id,
user_id=req_ctx.user_id,
Expand Down Expand Up @@ -483,6 +483,16 @@ async def replace_project(request: web.Request):
is_template=False,
app=request.app,
)
# Appends folder ID
user_specific_project_data_db = await db.get_user_specific_project_data_db(
project_uuid=path_params.project_id,
private_workspace_user_id_or_none=(
req_ctx.user_id
if user_project_permission.workspace_id is None
else None
),
)
data["folderId"] = user_specific_project_data_db.folder_id

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def convert_to_db_names(project_document_data: dict) -> dict:
exclude_keys = [
"tags",
"prjOwner",
"folderId",
] # No column for tags, prjOwner is a foreign key in db
for key, value in project_document_data.items():
if key not in exclude_keys:
Expand Down
50 changes: 40 additions & 10 deletions services/web/server/src/simcore_service_webserver/projects/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,12 @@
ProjectNodeResourcesInsufficientRightsError,
ProjectNotFoundError,
)
from .models import ProjectDB, ProjectDict, UserProjectAccessRights
from .models import (
ProjectDB,
ProjectDict,
UserProjectAccessRightsDB,
UserSpecificProjectDataDB,
)

_logger = logging.getLogger(__name__)

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

async def get_user_specific_project_data_db(
self, project_uuid: ProjectID, private_workspace_user_id_or_none: UserID | None
) -> UserSpecificProjectDataDB:
async with self.engine.acquire() as conn:
result = await conn.execute(
sa.select(
*self._SELECTION_PROJECT_DB_ARGS, projects_to_folders.c.folder_id
)
.select_from(
projects.join(
projects_to_folders,
(
(projects_to_folders.c.project_uuid == projects.c.uuid)
& (
projects_to_folders.c.user_id
== private_workspace_user_id_or_none
)
),
isouter=True,
)
)
.where(projects.c.uuid == f"{project_uuid}")
)
row = await result.fetchone()
if row is None:
raise ProjectNotFoundError(project_uuid=project_uuid)
return UserSpecificProjectDataDB.from_orm(row)

async def get_pure_project_access_rights_without_workspace(
self, user_id: UserID, project_uuid: ProjectID
) -> UserProjectAccessRights:
) -> UserProjectAccessRightsDB:
"""
Be careful what you want. You should use `get_user_project_access_rights` to get access rights on the
project. It depends on which context you are in, whether private or shared workspace.
Expand Down Expand Up @@ -597,7 +627,7 @@ async def get_pure_project_access_rights_without_workspace(
raise ProjectInvalidRightsError(
user_id=user_id, project_uuid=project_uuid
)
return UserProjectAccessRights.from_orm(row)
return UserProjectAccessRightsDB.from_orm(row)

async def replace_project(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from aiopg.sa.result import RowProxy
from models_library.basic_types import HttpUrlWithCustomMinLength
from models_library.folders import FolderID
from models_library.projects import ClassifierID, ProjectID
from models_library.projects_ui import StudyUI
from models_library.users import UserID
Expand Down Expand Up @@ -63,13 +64,31 @@ class Config:
)


class UserSpecificProjectDataDB(ProjectDB):
folder_id: FolderID | None

class Config:
orm_mode = True


assert set(ProjectDB.__fields__.keys()).issubset( # nosec
{c.name for c in projects.columns if c.name not in ["access_rights"]}
)


class UserProjectAccessRights(BaseModel):
class UserProjectAccessRightsDB(BaseModel):
uid: UserID
read: bool
write: bool
delete: bool

class Config:
orm_mode = True


class UserProjectAccessRightsWithWorkspace(BaseModel):
uid: UserID
workspace_id: WorkspaceID | None # None if it's a private workspace
read: bool
write: bool
delete: bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,18 +180,26 @@ async def get_project_for_user(
db = ProjectDBAPI.get_from_app_context(app)

product_name = await db.get_project_product(ProjectID(project_uuid))
await check_user_project_permission(
user_project_access = await check_user_project_permission(
app,
project_id=ProjectID(project_uuid),
user_id=user_id,
product_name=product_name,
permission=cast(PermissionStr, check_permissions),
)
workspace_is_private = user_project_access.workspace_id is None

project, project_type = await db.get_project(
project_uuid,
)

# add folder id to the project base on the user
user_specific_project_data_db = await db.get_user_specific_project_data_db(
project_uuid=ProjectID(project_uuid),
private_workspace_user_id_or_none=user_id if workspace_is_private else None,
)
project["folderId"] = user_specific_project_data_db.folder_id

# adds state if it is not a template
if include_state:
project = await add_project_states_for_user(
Expand Down
1 change: 1 addition & 0 deletions services/web/server/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ async def _creator(
# dynamic
"state",
"permalink",
"folderId",
]

for key in new_project:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ async def _assert_get_same_project(
# Optional fields are not part of reference 'project'
project_state = data.pop("state")
project_permalink = data.pop("permalink", None)
folder_id = data.pop("folderId", None)

assert data == project

Expand All @@ -177,6 +178,8 @@ async def _assert_get_same_project(
if project_permalink:
assert parse_obj_as(ProjectPermalink, project_permalink)

assert folder_id is None


async def _replace_project(
client: TestClient, project_update: dict, expected: HTTPStatus
Expand Down Expand Up @@ -222,6 +225,7 @@ async def test_list_projects(
# template project
project_state = data[0].pop("state")
project_permalink = data[0].pop("permalink")
folder_id = data[0].pop("folderId")

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

assert data[1] == user_project
assert ProjectState(**project_state)
assert project_permalink is None
assert folder_id is None

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

assert data[0] == user_project
assert not ProjectState(
Expand All @@ -261,6 +268,7 @@ async def test_list_projects(
# template project
project_state = data[0].pop("state")
project_permalink = data[0].pop("permalink")
folder_id = data[0].pop("folderId")

assert data[0] == template_project
assert not ProjectState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -920,6 +920,7 @@ async def test_get_active_project(
)
assert not error
assert ProjectState(**data.pop("state")).locked.value
data.pop("folderId")

user_project_last_change_date = user_project.pop("lastChangeDate")
data_last_change_date = data.pop("lastChangeDate")
Expand Down Expand Up @@ -1416,6 +1417,7 @@ async def test_open_shared_project_at_same_time(
num_assertions += 1
elif data:
project_status = ProjectState(**data.pop("state"))
data.pop("folderId")
assert data == shared_project
assert project_status.locked.value
assert project_status.locked.owner
Expand Down
Loading

0 comments on commit ec575af

Please sign in to comment.