Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ web-api: trashed resources include trashedBy with the primary GID of the user that trashed it #7052

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from datetime import datetime
from typing import NamedTuple
from typing import Annotated, Self

from pydantic import ConfigDict, PositiveInt, field_validator
from pydantic import ConfigDict, Field, field_validator

from ..access_rights import AccessRights
from ..basic_types import IDStr
from ..folders import FolderID
from ..folders import FolderDB, FolderID
from ..groups import GroupID
from ..utils.common_validators import null_or_none_str_to_none_validator
from ..workspaces import WorkspaceID
Expand All @@ -16,17 +16,33 @@ class FolderGet(OutputSchema):
folder_id: FolderID
parent_folder_id: FolderID | None = None
name: str

created_at: datetime
modified_at: datetime
trashed_at: datetime | None
trashed_by: Annotated[
GroupID | None, Field(description="The primary gid of the user who trashed")
]
owner: GroupID
workspace_id: WorkspaceID | None
my_access_rights: AccessRights


class FolderGetPage(NamedTuple):
items: list[FolderGet]
total: PositiveInt
@classmethod
def from_domain_model(
cls, folder_db: FolderDB, user_folder_access_rights: AccessRights
) -> Self:
return cls.model_construct(
folder_id=folder_db.folder_id,
parent_folder_id=folder_db.parent_folder_id,
name=folder_db.name,
created_at=folder_db.created,
modified_at=folder_db.modified,
trashed_at=folder_db.trashed,
trashed_by=folder_db.trashed_by_primary_gid,
owner=folder_db.created_by_gid,
workspace_id=folder_db.workspace_id,
my_access_rights=user_folder_access_rights,
)


class FolderCreateBodyParams(InputSchema):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
from typing import Annotated, Any, Literal, Self, TypeAlias

from common_library.dict_tools import remap_keys
from models_library.folders import FolderID
from models_library.utils._original_fastapi_encoders import jsonable_encoder
from models_library.workspaces import WorkspaceID
from pydantic import (
BeforeValidator,
ConfigDict,
Expand All @@ -29,13 +26,15 @@
from ..projects_access import AccessRights, GroupIDStr
from ..projects_state import ProjectState
from ..projects_ui import StudyUI
from ..utils._original_fastapi_encoders import jsonable_encoder
from ..utils.common_validators import (
empty_str_to_none_pre_validator,
none_to_empty_str_pre_validator,
null_or_none_str_to_none_validator,
)
from ..workspaces import WorkspaceID
from ._base import EmptyModel, InputSchema, OutputSchema
from .groups import GroupID
from .permalinks import ProjectPermalink


Expand Down Expand Up @@ -98,6 +97,9 @@ class ProjectGet(OutputSchema):
folder_id: FolderID | None

trashed_at: datetime | None
trashed_by: Annotated[
GroupID | None, Field(description="The primary gid of the user who trashed")
]

_empty_description = field_validator("description", mode="before")(
none_to_empty_str_pre_validator
Expand All @@ -109,8 +111,16 @@ class ProjectGet(OutputSchema):
def from_domain_model(cls, project_data: dict[str, Any]) -> Self:
return cls.model_validate(
remap_keys(
project_data,
rename={"trashed": "trashed_at"},
{
k: v
for k, v in project_data.items()
if k not in {"trashed_by", "trashedBy"}
},
rename={
"trashed": "trashed_at",
"trashed_by_primary_gid": "trashed_by",
"trashedByPrimaryGid": "trashedBy",
},
)
)

Expand All @@ -127,7 +137,8 @@ class ProjectReplace(InputSchema):
name: ShortTruncatedStr
description: LongTruncatedStr
thumbnail: Annotated[
HttpUrl | None, BeforeValidator(empty_str_to_none_pre_validator)
HttpUrl | None,
BeforeValidator(empty_str_to_none_pre_validator),
] = Field(default=None)
creation_date: DateTimeStr
last_change_date: DateTimeStr
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from datetime import datetime
from typing import Self
from typing import Annotated, Self

from pydantic import ConfigDict
from pydantic import ConfigDict, Field

from ..access_rights import AccessRights
from ..basic_types import IDStr
from ..groups import GroupID
from ..users import UserID
from ..workspaces import UserWorkspaceWithAccessRights, WorkspaceID
from ._base import InputSchema, OutputSchema

Expand All @@ -19,7 +18,9 @@ class WorkspaceGet(OutputSchema):
created_at: datetime
modified_at: datetime
trashed_at: datetime | None
trashed_by: UserID | None
trashed_by: Annotated[
GroupID | None, Field(description="The primary gid of the user who trashed")
]
my_access_rights: AccessRights
access_rights: dict[GroupID, AccessRights]

Expand All @@ -33,7 +34,7 @@ def from_domain_model(cls, wks: UserWorkspaceWithAccessRights) -> Self:
created_at=wks.created,
modified_at=wks.modified,
trashed_at=wks.trashed,
trashed_by=wks.trashed_by if wks.trashed else None,
trashed_by=wks.trashed_by_primary_gid if wks.trashed else None,
my_access_rights=wks.my_access_rights,
access_rights=wks.access_rights,
)
Expand Down
45 changes: 18 additions & 27 deletions packages/models-library/src/models_library/folders.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
from datetime import datetime
from enum import auto
from typing import TypeAlias
from typing import NamedTuple, TypeAlias

from pydantic import (
BaseModel,
ConfigDict,
Field,
PositiveInt,
ValidationInfo,
field_validator,
)
from pydantic import BaseModel, ConfigDict, PositiveInt, ValidationInfo, field_validator

from .access_rights import AccessRights
from .groups import GroupID
Expand Down Expand Up @@ -52,29 +45,27 @@ class FolderDB(BaseModel):
folder_id: FolderID
name: str
parent_folder_id: FolderID | None
created_by_gid: GroupID = Field(
...,
description="GID of the group that owns this wallet",
)
created: datetime = Field(
...,
description="Timestamp on creation",
)
modified: datetime = Field(
...,
description="Timestamp of last modification",
)
trashed: datetime | None = Field(
...,
)

user_id: UserID | None
workspace_id: WorkspaceID | None

created_by_gid: GroupID
created: datetime
modified: datetime

trashed: datetime | None
trashed_by: UserID | None
trashed_by_primary_gid: GroupID | None = None
trashed_explicitly: bool

user_id: UserID | None # owner?
workspace_id: WorkspaceID | None
model_config = ConfigDict(from_attributes=True)


class UserFolderAccessRightsDB(FolderDB):
my_access_rights: AccessRights

model_config = ConfigDict(from_attributes=True)


class Folder(NamedTuple):
folder_db: FolderDB
my_access_rights: AccessRights
7 changes: 6 additions & 1 deletion packages/models-library/src/models_library/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from .basic_regex import DATE_RE, UUID_RE_BASE
from .emails import LowerCaseEmailStr
from .groups import GroupID
from .projects_access import AccessRights, GroupIDStr
from .projects_nodes import Node
from .projects_nodes_io import NodeIDStr
Expand Down Expand Up @@ -106,7 +107,7 @@ class ProjectAtDB(BaseProjectModel):

@field_validator("project_type", mode="before")
@classmethod
def convert_sql_alchemy_enum(cls, v):
def _convert_sql_alchemy_enum(cls, v):
if isinstance(v, Enum):
return v.value
return v
Expand Down Expand Up @@ -185,8 +186,12 @@ class Project(BaseProjectModel):

trashed: datetime | None = None
trashed_by: Annotated[UserID | None, Field(alias="trashedBy")] = None
trashed_by_primary_gid: Annotated[
GroupID | None, Field(alias="trashedByPrimaryGid")
] = None
trashed_explicitly: Annotated[bool, Field(alias="trashedExplicitly")] = False

model_config = ConfigDict(
# NOTE: this is a security measure until we get rid of the ProjectDict variants
extra="forbid",
)
17 changes: 9 additions & 8 deletions packages/models-library/src/models_library/workspaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class Workspace(BaseModel):
workspace_id: WorkspaceID
name: str
description: str | None
owner_primary_gid: PositiveInt = Field(
owner_primary_gid: GroupID = Field(
...,
description="GID of the group that owns this wallet",
)
Expand All @@ -62,6 +62,14 @@ class Workspace(BaseModel):
)
trashed: datetime | None
trashed_by: UserID | None
trashed_by_primary_gid: GroupID | None = None

model_config = ConfigDict(from_attributes=True)


class UserWorkspaceWithAccessRights(Workspace):
my_access_rights: AccessRights
access_rights: dict[GroupID, AccessRights]

model_config = ConfigDict(from_attributes=True)

Expand All @@ -72,10 +80,3 @@ class WorkspaceUpdates(BaseModel):
thumbnail: str | None = None
trashed: datetime | None = None
trashed_by: UserID | None = None


class UserWorkspaceWithAccessRights(Workspace):
my_access_rights: AccessRights
access_rights: dict[GroupID, AccessRights]

model_config = ConfigDict(from_attributes=True)
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

@pytest.mark.parametrize(
"api_call",
(NEW_PROJECT, CREATE_FROM_SERVICE, CREATE_FROM_TEMPLATE),
[NEW_PROJECT, CREATE_FROM_SERVICE, CREATE_FROM_TEMPLATE],
ids=lambda c: c.name,
)
def test_create_project_schemas(api_call: HttpApiCallCapture):
Expand All @@ -45,7 +45,7 @@ def test_create_project_schemas(api_call: HttpApiCallCapture):

@pytest.mark.parametrize(
"api_call",
(LIST_PROJECTS,),
[LIST_PROJECTS],
ids=lambda c: c.name,
)
def test_list_project_schemas(api_call: HttpApiCallCapture):
Expand All @@ -59,7 +59,7 @@ def test_list_project_schemas(api_call: HttpApiCallCapture):

@pytest.mark.parametrize(
"api_call",
(GET_PROJECT, CREATE_FROM_TEMPLATE__TASK_RESULT),
[GET_PROJECT, CREATE_FROM_TEMPLATE__TASK_RESULT],
ids=lambda c: c.name,
)
def test_get_project_schemas(api_call: HttpApiCallCapture):
Expand All @@ -74,7 +74,7 @@ def test_get_project_schemas(api_call: HttpApiCallCapture):

@pytest.mark.parametrize(
"api_call",
(REPLACE_PROJECT, REPLACE_PROJECT_ON_MODIFIED),
[REPLACE_PROJECT, REPLACE_PROJECT_ON_MODIFIED],
ids=lambda c: c.name,
)
def test_replace_project_schemas(api_call: HttpApiCallCapture):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class ProjectType(enum.Enum):
JSONB,
nullable=False,
server_default=sa.text("'{}'::jsonb"),
doc="Read/write/delete access rights of each group (gid) on this project",
doc="DEPRECATED: Read/write/delete access rights of each group (gid) on this project",
),
sa.Column(
"workbench",
Expand Down
Loading
Loading