From 2a3ad00bc76f89044c7e7bc62e0ab1d1940d3c75 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 2 Oct 2024 09:29:45 +0200 Subject: [PATCH 01/12] fix workbench and accessrights --- .../src/simcore_service_webserver/projects/models.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/models.py b/services/web/server/src/simcore_service_webserver/projects/models.py index 8f4a13c172b..a2a3ea2b3a9 100644 --- a/services/web/server/src/simcore_service_webserver/projects/models.py +++ b/services/web/server/src/simcore_service_webserver/projects/models.py @@ -5,9 +5,10 @@ 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 import ClassifierID, NodesDict, ProjectID +from models_library.projects_access import AccessRights from models_library.projects_ui import StudyUI -from models_library.users import UserID +from models_library.users import GroupID, UserID from models_library.utils.common_validators import ( empty_str_to_none_pre_validator, none_to_empty_str_pre_validator, @@ -66,6 +67,8 @@ class Config: class UserSpecificProjectDataDB(ProjectDB): folder_id: FolderID | None + workbench: NodesDict + access_rights: dict[GroupID, AccessRights] class Config: orm_mode = True From 535f727e759b9fceccd24b11d644acf7bf3ffc6d Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 2 Oct 2024 09:49:46 +0200 Subject: [PATCH 02/12] renaming model --- .../src/simcore_postgres_database/models/projects.py | 1 - .../src/simcore_service_webserver/projects/db.py | 10 +++++----- .../src/simcore_service_webserver/projects/models.py | 2 +- .../test_workspaces__list_projects_full_search.py | 2 ++ 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/projects.py b/packages/postgres-database/src/simcore_postgres_database/models/projects.py index ae77ea5c5d0..176d4c7d9fd 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/projects.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/projects.py @@ -67,7 +67,6 @@ class ProjectType(enum.Enum): onupdate="CASCADE", ondelete="RESTRICT", ), - nullable=True, doc="Project's owner", index=True, ), diff --git a/services/web/server/src/simcore_service_webserver/projects/db.py b/services/web/server/src/simcore_service_webserver/projects/db.py index 0e8b8c242fa..e7b8ac66992 100644 --- a/services/web/server/src/simcore_service_webserver/projects/db.py +++ b/services/web/server/src/simcore_service_webserver/projects/db.py @@ -94,7 +94,7 @@ ProjectDB, ProjectDict, UserProjectAccessRightsDB, - UserSpecificProjectDataDB, + UserSpecificListProjectDB, ) _logger = logging.getLogger(__name__) @@ -617,8 +617,8 @@ async def list_projects_full_search( list_query = combined_query.offset(offset).limit(limit) result = await conn.execute(list_query) rows = await result.fetchall() or [] - results: list[UserSpecificProjectDataDB] = [ - UserSpecificProjectDataDB.from_orm(row) for row in rows + results: list[UserSpecificListProjectDB] = [ + UserSpecificListProjectDB.from_orm(row) for row in rows ] # NOTE: Additional adjustments to make it back compatible @@ -703,7 +703,7 @@ async def get_project_db(self, project_uuid: ProjectID) -> ProjectDB: async def get_user_specific_project_data_db( self, project_uuid: ProjectID, private_workspace_user_id_or_none: UserID | None - ) -> UserSpecificProjectDataDB: + ) -> UserSpecificListProjectDB: async with self.engine.acquire() as conn: result = await conn.execute( sa.select( @@ -727,7 +727,7 @@ async def get_user_specific_project_data_db( row = await result.fetchone() if row is None: raise ProjectNotFoundError(project_uuid=project_uuid) - return UserSpecificProjectDataDB.from_orm(row) + return UserSpecificListProjectDB.from_orm(row) async def get_pure_project_access_rights_without_workspace( self, user_id: UserID, project_uuid: ProjectID diff --git a/services/web/server/src/simcore_service_webserver/projects/models.py b/services/web/server/src/simcore_service_webserver/projects/models.py index a2a3ea2b3a9..f38acb5ed38 100644 --- a/services/web/server/src/simcore_service_webserver/projects/models.py +++ b/services/web/server/src/simcore_service_webserver/projects/models.py @@ -65,7 +65,7 @@ class Config: ) -class UserSpecificProjectDataDB(ProjectDB): +class UserSpecificListProjectDB(ProjectDB): folder_id: FolderID | None workbench: NodesDict access_rights: dict[GroupID, AccessRights] diff --git a/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces__list_projects_full_search.py b/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces__list_projects_full_search.py index 1340630e489..783cc83f489 100644 --- a/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces__list_projects_full_search.py +++ b/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces__list_projects_full_search.py @@ -87,6 +87,8 @@ async def test_workspaces__list_projects_full_search( assert data[0]["uuid"] == project_1["uuid"] assert data[0]["workspaceId"] == added_workspace["workspaceId"] assert data[0]["folderId"] is None + assert data[0]["workbench"] + assert data[0]["accessRights"] # Create projects in private workspace project_data = deepcopy(fake_project) From 6c7f73297c80d03453bb87be7f52315e94b93442 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 2 Oct 2024 13:29:21 +0200 Subject: [PATCH 03/12] fixes --- .../servicelib/aiohttp/requests_validation.py | 12 +++- .../projects/_crud_api_read.py | 44 ++++++++++++-- .../projects/_crud_handlers.py | 4 +- .../projects/_crud_handlers_models.py | 13 ++++- .../simcore_service_webserver/projects/db.py | 57 ++++++++++++------- .../projects/models.py | 9 +-- ...t_workspaces__list_projects_full_search.py | 2 +- 7 files changed, 107 insertions(+), 34 deletions(-) diff --git a/packages/service-library/src/servicelib/aiohttp/requests_validation.py b/packages/service-library/src/servicelib/aiohttp/requests_validation.py index 2fd5d0e41f0..a1d6a088878 100644 --- a/packages/service-library/src/servicelib/aiohttp/requests_validation.py +++ b/packages/service-library/src/servicelib/aiohttp/requests_validation.py @@ -8,6 +8,7 @@ """ import json.decoder +from collections import defaultdict from collections.abc import Iterator from contextlib import contextmanager from typing import TypeAlias, TypeVar, Union @@ -166,7 +167,16 @@ def parse_request_query_parameters_as( resource_name=request.rel_url.path, use_error_v1=use_enveloped_error_v1, ): - data = dict(request.query) + tmp_data = defaultdict(list) + for key, value in request.query.items(): + tmp_data[key].append(value) + # Convert defaultdict to a normal dictionary + # And if a key has only one value, store it as that value instead of a list + data = { + key: value if len(value) > 1 else value[0] + for key, value in tmp_data.items() + } + if hasattr(parameters_schema_cls, "parse_obj"): return parameters_schema_cls.parse_obj(data) model: ModelClass = parse_obj_as(parameters_schema_cls, data) diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py b/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py index b3f2f928329..ae6fa7435c2 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py @@ -143,25 +143,61 @@ async def list_projects( # pylint: disable=too-many-arguments async def list_projects_full_search( - app, + request, *, user_id: UserID, product_name: str, offset: NonNegativeInt, limit: int, text: str | None, + order_by: OrderBy, + tag_id: int | list[int] | None, ) -> tuple[list[ProjectDict], int]: - db = ProjectDBAPI.get_from_app_context(app) + if tag_id is None: + tag_ids = [] + elif isinstance(tag_id, int): + tag_ids = [tag_id] + elif isinstance(tag_id, list): + tag_ids = tag_id + + db = ProjectDBAPI.get_from_app_context(request.app) + + user_available_services: list[dict] = await get_services_for_user_in_product( + request.app, user_id, product_name, only_key_versions=True + ) - total_number_projects, db_projects = await db.list_projects_full_search( + ( + db_projects, + db_project_types, + total_number_projects, + ) = await db.list_projects_full_search( user_id=user_id, product_name=product_name, + filter_by_services=user_available_services, text=text, offset=offset, limit=limit, + order_by=order_by, + tag_ids=tag_ids, + ) + + projects: list[ProjectDict] = await logged_gather( + *( + _append_fields( + request, + user_id=user_id, + project=prj, + is_template=prj_type == ProjectTypeDB.TEMPLATE, + workspace_access_rights=None, + model_schema_cls=ProjectListItem, + ) + for prj, prj_type in zip(db_projects, db_project_types) + ), + reraise=True, + max_concurrency=100, ) - return db_projects, total_number_projects + return projects, total_number_projects async def get_project( diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py index b56a1764f7d..68b52b92b1e 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py @@ -238,12 +238,14 @@ async def list_projects_full_search(request: web.Request): ) projects, total_number_of_projects = await _crud_api_read.list_projects_full_search( - request.app, + request, user_id=req_ctx.user_id, product_name=req_ctx.product_name, limit=query_params.limit, offset=query_params.offset, text=query_params.text, + order_by=query_params.order_by, + tag_id=query_params.tag_id, ) page = Page[ProjectDict].parse_obj( diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py index 967ca8fe090..9aba90c2b60 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py @@ -123,7 +123,7 @@ def search_check_empty_string(cls, v): )(null_or_none_str_to_none_validator) -class ProjectListWithJsonStrParams(ProjectListParams): +class ProjectListWithOrderByParams(BaseModel): order_by: Json[OrderBy] = Field( # pylint: disable=unsubscriptable-object default=OrderBy(field=IDStr("last_change_date"), direction=OrderDirection.DESC), description="Order by field (type|uuid|name|description|prj_owner|creation_date|last_change_date) and direction (asc|desc). The default sorting order is ascending.", @@ -151,17 +151,26 @@ class Config: extra = Extra.forbid +class ProjectListWithJsonStrParams(ProjectListParams, ProjectListWithOrderByParams): + ... + + class ProjectActiveParams(BaseModel): client_session_id: str -class ProjectListFullSearchParams(PageQueryParameters): +class ProjectListFullSearchParams(PageQueryParameters, ProjectListWithOrderByParams): text: str | None = Field( default=None, description="Multi column full text search, across all folders and workspaces", max_length=100, example="My Project", ) + tag_id: int | list[int] | None = Field( + default=None, + description="Search by tag id (multiple tag id parameters might be provided)", + example="1", + ) _empty_is_none = validator("text", allow_reuse=True, pre=True)( empty_str_to_none_pre_validator diff --git a/services/web/server/src/simcore_service_webserver/projects/db.py b/services/web/server/src/simcore_service_webserver/projects/db.py index e7b8ac66992..e446f2d7224 100644 --- a/services/web/server/src/simcore_service_webserver/projects/db.py +++ b/services/web/server/src/simcore_service_webserver/projects/db.py @@ -40,6 +40,7 @@ from simcore_postgres_database.models.groups import user_to_groups from simcore_postgres_database.models.project_to_groups import project_to_groups from simcore_postgres_database.models.projects_nodes import projects_nodes +from simcore_postgres_database.models.projects_tags import projects_tags from simcore_postgres_database.models.projects_to_folders import projects_to_folders from simcore_postgres_database.models.projects_to_products import projects_to_products from simcore_postgres_database.models.wallets import wallets @@ -94,7 +95,7 @@ ProjectDB, ProjectDict, UserProjectAccessRightsDB, - UserSpecificListProjectDB, + UserSpecificProjectDataDB, ) _logger = logging.getLogger(__name__) @@ -494,10 +495,15 @@ async def list_projects_full_search( *, user_id: PositiveInt, product_name: ProductName, + filter_by_services: list[dict] | None = None, text: str | None = None, offset: int | None = 0, limit: int | None = None, - ) -> tuple[int, list[ProjectDict]]: + tag_ids: list[int], + order_by: OrderBy = OrderBy( + field=IDStr("last_change_date"), direction=OrderDirection.DESC + ), + ) -> tuple[list[dict[str, Any]], list[ProjectType], int]: async with self.engine.acquire() as conn: user_groups: list[RowProxy] = await self._list_user_groups(conn, user_id) @@ -520,6 +526,13 @@ async def list_projects_full_search( ).group_by(workspaces_access_rights.c.workspace_id) ).subquery("workspace_access_rights_subquery") + # project_tags_subquery = ( + # sa.select( + # projects_tags.c.project_id, + # sa.func.array_agg(projects_tags.c.tag_id).label("tags"), + # ).group_by(projects_tags.c.project_id) + # ).subquery("project_tags_subquery") + private_workspace_query = ( sa.select( *[ @@ -530,6 +543,7 @@ async def list_projects_full_search( self.access_rights_subquery.c.access_rights, projects_to_products.c.product_name, projects_to_folders.c.folder_id, + # project_tags_subquery.c.tags, ) .select_from( projects.join(self.access_rights_subquery, isouter=True) @@ -542,6 +556,7 @@ async def list_projects_full_search( ), isouter=True, ) + # .join(project_tags_subquery, isouter=True) ) .where( ( @@ -614,23 +629,27 @@ async def list_projects_full_search( count_query = sa.select(func.count()).select_from(combined_query) total_count = await conn.scalar(count_query) - list_query = combined_query.offset(offset).limit(limit) - result = await conn.execute(list_query) - rows = await result.fetchall() or [] - results: list[UserSpecificListProjectDB] = [ - UserSpecificListProjectDB.from_orm(row) for row in rows - ] + if order_by.direction == OrderDirection.ASC: + combined_query = combined_query.order_by( + sa.asc(getattr(projects.c, order_by.field)) + ) + else: + combined_query = combined_query.order_by( + sa.desc(getattr(projects.c, order_by.field)) + ) - # NOTE: Additional adjustments to make it back compatible - list_of_converted_projects: list[ProjectDict] = [] - for project in results: - user_email = await self._get_user_email(conn, project.prj_owner) - converted_project = convert_to_schema_names(project.dict(), user_email) - converted_project["tags"] = [] # <-- Tags not needed for now - converted_project["state"] = None - list_of_converted_projects.append(converted_project) + prjs, prj_types = await self._execute_without_permission_check( + conn, + user_id=user_id, + select_projects_query=combined_query.offset(offset).limit(limit), + filter_by_services=filter_by_services, + ) - return cast(int, total_count), list_of_converted_projects + return ( + prjs, + prj_types, + cast(int, total_count), + ) async def list_projects_uuids(self, user_id: int) -> list[str]: async with self.engine.acquire() as conn: @@ -703,7 +722,7 @@ async def get_project_db(self, project_uuid: ProjectID) -> ProjectDB: async def get_user_specific_project_data_db( self, project_uuid: ProjectID, private_workspace_user_id_or_none: UserID | None - ) -> UserSpecificListProjectDB: + ) -> UserSpecificProjectDataDB: async with self.engine.acquire() as conn: result = await conn.execute( sa.select( @@ -727,7 +746,7 @@ async def get_user_specific_project_data_db( row = await result.fetchone() if row is None: raise ProjectNotFoundError(project_uuid=project_uuid) - return UserSpecificListProjectDB.from_orm(row) + return UserSpecificProjectDataDB.from_orm(row) async def get_pure_project_access_rights_without_workspace( self, user_id: UserID, project_uuid: ProjectID diff --git a/services/web/server/src/simcore_service_webserver/projects/models.py b/services/web/server/src/simcore_service_webserver/projects/models.py index f38acb5ed38..8f4a13c172b 100644 --- a/services/web/server/src/simcore_service_webserver/projects/models.py +++ b/services/web/server/src/simcore_service_webserver/projects/models.py @@ -5,10 +5,9 @@ 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, NodesDict, ProjectID -from models_library.projects_access import AccessRights +from models_library.projects import ClassifierID, ProjectID from models_library.projects_ui import StudyUI -from models_library.users import GroupID, UserID +from models_library.users import UserID from models_library.utils.common_validators import ( empty_str_to_none_pre_validator, none_to_empty_str_pre_validator, @@ -65,10 +64,8 @@ class Config: ) -class UserSpecificListProjectDB(ProjectDB): +class UserSpecificProjectDataDB(ProjectDB): folder_id: FolderID | None - workbench: NodesDict - access_rights: dict[GroupID, AccessRights] class Config: orm_mode = True diff --git a/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces__list_projects_full_search.py b/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces__list_projects_full_search.py index 783cc83f489..1689fde7e57 100644 --- a/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces__list_projects_full_search.py +++ b/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces__list_projects_full_search.py @@ -80,7 +80,7 @@ async def test_workspaces__list_projects_full_search( # List project with full search base_url = client.app.router["list_projects_full_search"].url_for() - url = base_url.with_query({"text": "solution"}) + url = base_url.with_query([("text", "solution"), ("tag_id", 1), ("tag_id", 2)]) resp = await client.get(url) data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 From fc3ae8658456e06630dc87c07522598fbf90227c Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 2 Oct 2024 14:58:41 +0200 Subject: [PATCH 04/12] fixes --- .../simcore_service_webserver/projects/db.py | 40 +++++++++++++++---- ...t_workspaces__list_projects_full_search.py | 2 +- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/db.py b/services/web/server/src/simcore_service_webserver/projects/db.py index e446f2d7224..158ab54c762 100644 --- a/services/web/server/src/simcore_service_webserver/projects/db.py +++ b/services/web/server/src/simcore_service_webserver/projects/db.py @@ -526,12 +526,12 @@ async def list_projects_full_search( ).group_by(workspaces_access_rights.c.workspace_id) ).subquery("workspace_access_rights_subquery") - # project_tags_subquery = ( - # sa.select( - # projects_tags.c.project_id, - # sa.func.array_agg(projects_tags.c.tag_id).label("tags"), - # ).group_by(projects_tags.c.project_id) - # ).subquery("project_tags_subquery") + project_tags_subquery = ( + sa.select( + projects_tags.c.project_id, + sa.func.array_agg(projects_tags.c.tag_id).label("tags"), + ).group_by(projects_tags.c.project_id) + ).subquery("project_tags_subquery") private_workspace_query = ( sa.select( @@ -543,7 +543,10 @@ async def list_projects_full_search( self.access_rights_subquery.c.access_rights, projects_to_products.c.product_name, projects_to_folders.c.folder_id, - # project_tags_subquery.c.tags, + sa.func.coalesce( + project_tags_subquery.c.tags, + sa.cast(sa.text("'{}'"), sa.ARRAY(sa.Integer)), + ).label("tags"), ) .select_from( projects.join(self.access_rights_subquery, isouter=True) @@ -556,7 +559,7 @@ async def list_projects_full_search( ), isouter=True, ) - # .join(project_tags_subquery, isouter=True) + .join(project_tags_subquery, isouter=True) ) .where( ( @@ -577,6 +580,14 @@ async def list_projects_full_search( ) ) + if tag_ids: + private_workspace_query = private_workspace_query.where( + sa.func.coalesce( + project_tags_subquery.c.tags, + sa.cast(sa.text("'{}'"), sa.ARRAY(sa.Integer)), + ).op("@>")(tag_ids) + ) + shared_workspace_query = ( sa.select( *[ @@ -587,6 +598,10 @@ async def list_projects_full_search( workspace_access_rights_subquery.c.access_rights, projects_to_products.c.product_name, projects_to_folders.c.folder_id, + sa.func.coalesce( + project_tags_subquery.c.tags, + sa.cast(sa.text("'{}'"), sa.ARRAY(sa.Integer)), + ).label("tags"), ) .select_from( projects.join( @@ -603,6 +618,7 @@ async def list_projects_full_search( ), isouter=True, ) + .join(project_tags_subquery, isouter=True) ) .where( ( @@ -622,6 +638,14 @@ async def list_projects_full_search( ) ) + if tag_ids: + shared_workspace_query = shared_workspace_query.where( + sa.func.coalesce( + project_tags_subquery.c.tags, + sa.cast(sa.text("'{}'"), sa.ARRAY(sa.Integer)), + ).op("@>")(tag_ids) + ) + combined_query = sa.union_all( private_workspace_query, shared_workspace_query ) diff --git a/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces__list_projects_full_search.py b/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces__list_projects_full_search.py index 1689fde7e57..783cc83f489 100644 --- a/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces__list_projects_full_search.py +++ b/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces__list_projects_full_search.py @@ -80,7 +80,7 @@ async def test_workspaces__list_projects_full_search( # List project with full search base_url = client.app.router["list_projects_full_search"].url_for() - url = base_url.with_query([("text", "solution"), ("tag_id", 1), ("tag_id", 2)]) + url = base_url.with_query({"text": "solution"}) resp = await client.get(url) data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 From 23ea441e693e9604a271a807f12fe04fdb3dcf94 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 2 Oct 2024 15:14:56 +0200 Subject: [PATCH 05/12] tag_ids_list --- .../projects/_crud_api_read.py | 11 ++--------- .../projects/_crud_handlers.py | 3 ++- .../projects/_crud_handlers_models.py | 6 +++--- .../src/simcore_service_webserver/projects/db.py | 10 +++++----- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py b/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py index ae6fa7435c2..a15dfee8a1c 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py @@ -151,15 +151,8 @@ async def list_projects_full_search( limit: int, text: str | None, order_by: OrderBy, - tag_id: int | list[int] | None, + tag_ids_list: list[int] | None, ) -> tuple[list[ProjectDict], int]: - if tag_id is None: - tag_ids = [] - elif isinstance(tag_id, int): - tag_ids = [tag_id] - elif isinstance(tag_id, list): - tag_ids = tag_id - db = ProjectDBAPI.get_from_app_context(request.app) user_available_services: list[dict] = await get_services_for_user_in_product( @@ -178,7 +171,7 @@ async def list_projects_full_search( offset=offset, limit=limit, order_by=order_by, - tag_ids=tag_ids, + tag_ids_list=tag_ids_list, ) projects: list[ProjectDict] = await logged_gather( diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py index 68b52b92b1e..deade4ad80a 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py @@ -236,6 +236,7 @@ async def list_projects_full_search(request: web.Request): query_params: ProjectListFullSearchParams = parse_request_query_parameters_as( ProjectListFullSearchParams, request ) + tag_ids_list = list(map(int, query_params.tag_ids.split(","))) projects, total_number_of_projects = await _crud_api_read.list_projects_full_search( request, @@ -245,7 +246,7 @@ async def list_projects_full_search(request: web.Request): offset=query_params.offset, text=query_params.text, order_by=query_params.order_by, - tag_id=query_params.tag_id, + tag_ids_list=tag_ids_list, ) page = Page[ProjectDict].parse_obj( diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py index 9aba90c2b60..753d96b5ae3 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py @@ -166,10 +166,10 @@ class ProjectListFullSearchParams(PageQueryParameters, ProjectListWithOrderByPar max_length=100, example="My Project", ) - tag_id: int | list[int] | None = Field( + tag_ids: str | None = Field( default=None, - description="Search by tag id (multiple tag id parameters might be provided)", - example="1", + description="Search by tag ID (multiple tag IDs may be provided separated by column)", + example="1,3", ) _empty_is_none = validator("text", allow_reuse=True, pre=True)( diff --git a/services/web/server/src/simcore_service_webserver/projects/db.py b/services/web/server/src/simcore_service_webserver/projects/db.py index 158ab54c762..bde77e1b186 100644 --- a/services/web/server/src/simcore_service_webserver/projects/db.py +++ b/services/web/server/src/simcore_service_webserver/projects/db.py @@ -499,7 +499,7 @@ async def list_projects_full_search( text: str | None = None, offset: int | None = 0, limit: int | None = None, - tag_ids: list[int], + tag_ids_list: list[int] | None, order_by: OrderBy = OrderBy( field=IDStr("last_change_date"), direction=OrderDirection.DESC ), @@ -580,12 +580,12 @@ async def list_projects_full_search( ) ) - if tag_ids: + if tag_ids_list: private_workspace_query = private_workspace_query.where( sa.func.coalesce( project_tags_subquery.c.tags, sa.cast(sa.text("'{}'"), sa.ARRAY(sa.Integer)), - ).op("@>")(tag_ids) + ).op("@>")(tag_ids_list) ) shared_workspace_query = ( @@ -638,12 +638,12 @@ async def list_projects_full_search( ) ) - if tag_ids: + if tag_ids_list: shared_workspace_query = shared_workspace_query.where( sa.func.coalesce( project_tags_subquery.c.tags, sa.cast(sa.text("'{}'"), sa.ARRAY(sa.Integer)), - ).op("@>")(tag_ids) + ).op("@>")(tag_ids_list) ) combined_query = sa.union_all( From 14e825124739caccefe610007424c5616b7adaf8 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 2 Oct 2024 15:21:01 +0200 Subject: [PATCH 06/12] tag_ids_list --- .../src/simcore_service_webserver/projects/_crud_api_read.py | 2 +- .../src/simcore_service_webserver/projects/_crud_handlers.py | 5 ++++- .../web/server/src/simcore_service_webserver/projects/db.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py b/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py index a15dfee8a1c..21802e9841d 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py @@ -151,7 +151,7 @@ async def list_projects_full_search( limit: int, text: str | None, order_by: OrderBy, - tag_ids_list: list[int] | None, + tag_ids_list: list[int], ) -> tuple[list[ProjectDict], int]: db = ProjectDBAPI.get_from_app_context(request.app) diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py index deade4ad80a..ca4adb58c8c 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py @@ -236,7 +236,10 @@ async def list_projects_full_search(request: web.Request): query_params: ProjectListFullSearchParams = parse_request_query_parameters_as( ProjectListFullSearchParams, request ) - tag_ids_list = list(map(int, query_params.tag_ids.split(","))) + if query_params.tag_ids: + tag_ids_list = list(map(int, query_params.tag_ids.split(","))) + else: + tag_ids_list = [] projects, total_number_of_projects = await _crud_api_read.list_projects_full_search( request, diff --git a/services/web/server/src/simcore_service_webserver/projects/db.py b/services/web/server/src/simcore_service_webserver/projects/db.py index bde77e1b186..10487a80e90 100644 --- a/services/web/server/src/simcore_service_webserver/projects/db.py +++ b/services/web/server/src/simcore_service_webserver/projects/db.py @@ -499,7 +499,7 @@ async def list_projects_full_search( text: str | None = None, offset: int | None = 0, limit: int | None = None, - tag_ids_list: list[int] | None, + tag_ids_list: list[int], order_by: OrderBy = OrderBy( field=IDStr("last_change_date"), direction=OrderDirection.DESC ), From 78e203f098d44c6b6a22ab349694086207596677 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 2 Oct 2024 17:04:34 +0200 Subject: [PATCH 07/12] fix --- api/specs/web-server/_projects_crud.py | 9 ++- .../api/v0/openapi.yaml | 70 ++++++++++++++++++- .../projects/_crud_handlers.py | 14 +++- .../projects/_crud_handlers_models.py | 8 ++- .../projects/_db_utils.py | 4 +- .../projects/exceptions.py | 4 ++ 6 files changed, 101 insertions(+), 8 deletions(-) diff --git a/api/specs/web-server/_projects_crud.py b/api/specs/web-server/_projects_crud.py index 1c76ff4230d..46073f921fc 100644 --- a/api/specs/web-server/_projects_crud.py +++ b/api/specs/web-server/_projects_crud.py @@ -142,10 +142,17 @@ async def clone_project( @router.get( "/projects:search", - response_model=Page[ProjectListItem], + response_model=Page[ProjectListFullSearchParams], ) async def list_projects_full_search( _params: Annotated[ProjectListFullSearchParams, Depends()], + order_by: Annotated[ + Json, + Query( + description="Order by field (type|uuid|name|description|prj_owner|creation_date|last_change_date) and direction (asc|desc). The default sorting order is ascending.", + example='{"field": "last_change_date", "direction": "desc"}', + ), + ] = ('{"field": "last_change_date", "direction": "desc"}',), ): ... diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index 90aeff0748b..186734f5a82 100644 --- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml +++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml @@ -3298,6 +3298,18 @@ paths: summary: List Projects Full Search operationId: list_projects_full_search parameters: + - description: Order by field (type|uuid|name|description|prj_owner|creation_date|last_change_date) + and direction (asc|desc). The default sorting order is ascending. + required: false + schema: + title: Order By + description: Order by field (type|uuid|name|description|prj_owner|creation_date|last_change_date) + and direction (asc|desc). The default sorting order is ascending. + default: + - '{"field": "last_change_date", "direction": "desc"}' + example: '{"field": "last_change_date", "direction": "desc"}' + name: order_by + in: query - required: false schema: title: Limit @@ -3323,13 +3335,19 @@ paths: type: string name: text in: query + - required: false + schema: + title: Tag Ids + type: string + name: tag_ids + in: query responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/Page_ProjectListItem_' + $ref: '#/components/schemas/Page_ProjectListFullSearchParams_' /v0/projects/{project_id}/inactivity: get: tags: @@ -9555,6 +9573,25 @@ components: $ref: '#/components/schemas/ProjectIterationResultItem' additionalProperties: false description: Paginated response model of ItemTs + Page_ProjectListFullSearchParams_: + title: Page[ProjectListFullSearchParams] + required: + - _meta + - _links + - data + type: object + properties: + _meta: + $ref: '#/components/schemas/PageMetaInfoLimitOffset' + _links: + $ref: '#/components/schemas/PageLinks' + data: + title: Data + type: array + items: + $ref: '#/components/schemas/ProjectListFullSearchParams' + additionalProperties: false + description: Paginated response model of ItemTs Page_ProjectListItem_: title: Page[ProjectListItem] required: @@ -10414,6 +10451,37 @@ components: format: uri results: $ref: '#/components/schemas/ExtractedResults' + ProjectListFullSearchParams: + title: ProjectListFullSearchParams + type: object + properties: + limit: + title: Limit + exclusiveMaximum: true + minimum: 1 + type: integer + description: maximum number of items to return (pagination) + default: 20 + maximum: 50 + offset: + title: Offset + minimum: 0 + type: integer + description: index to the first item to return (pagination) + default: 0 + text: + title: Text + maxLength: 100 + type: string + description: Multi column full text search, across all folders and workspaces + example: My Project + tag_ids: + title: Tag Ids + type: string + description: Search by tag ID (multiple tag IDs may be provided separated + by column) + example: 1,3 + description: Use as pagination options in query parameters ProjectListItem: title: ProjectListItem required: diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py index ca4adb58c8c..7db0a59e5a2 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py @@ -25,7 +25,7 @@ from models_library.rest_pagination_utils import paginate_data from models_library.utils.fastapi_encoders import jsonable_encoder from models_library.utils.json_serialization import json_dumps -from pydantic import parse_obj_as +from pydantic import ValidationError, parse_obj_as from servicelib.aiohttp.long_running_tasks.server import start_long_running_task from servicelib.aiohttp.requests_validation import ( parse_request_body_as, @@ -69,6 +69,7 @@ ProjectInvalidUsageError, ProjectNotFoundError, ProjectOwnerNotFoundInTheProjectAccessRightsError, + WrongTagIdsInQueryError, ) from .lock import get_project_locked_state from .models import ProjectDict @@ -101,7 +102,10 @@ async def _wrapper(request: web.Request) -> web.StreamResponse: WorkspaceNotFoundError, ) as exc: raise web.HTTPNotFound(reason=f"{exc}") from exc - except ProjectOwnerNotFoundInTheProjectAccessRightsError as exc: + except ( + ProjectOwnerNotFoundInTheProjectAccessRightsError, + WrongTagIdsInQueryError, + ) as exc: raise web.HTTPBadRequest(reason=f"{exc}") from exc except ( ProjectInvalidRightsError, @@ -237,7 +241,11 @@ async def list_projects_full_search(request: web.Request): ProjectListFullSearchParams, request ) if query_params.tag_ids: - tag_ids_list = list(map(int, query_params.tag_ids.split(","))) + try: + tag_ids_list = list(map(int, query_params.tag_ids.split(","))) + parse_obj_as(list[int], tag_ids_list) + except ValidationError as exc: + raise WrongTagIdsInQueryError from exc else: tag_ids_list = [] diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py index 753d96b5ae3..614f2325f1a 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py @@ -159,7 +159,7 @@ class ProjectActiveParams(BaseModel): client_session_id: str -class ProjectListFullSearchParams(PageQueryParameters, ProjectListWithOrderByParams): +class ProjectListFullSearchParams(PageQueryParameters): text: str | None = Field( default=None, description="Multi column full text search, across all folders and workspaces", @@ -175,3 +175,9 @@ class ProjectListFullSearchParams(PageQueryParameters, ProjectListWithOrderByPar _empty_is_none = validator("text", allow_reuse=True, pre=True)( empty_str_to_none_pre_validator ) + + +class ProjectListFullSearchWithJsonStrParams( + ProjectListFullSearchParams, ProjectListWithOrderByParams +): + ... diff --git a/services/web/server/src/simcore_service_webserver/projects/_db_utils.py b/services/web/server/src/simcore_service_webserver/projects/_db_utils.py index aa0f6ce8048..8bda162ab6f 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_db_utils.py +++ b/services/web/server/src/simcore_service_webserver/projects/_db_utils.py @@ -20,7 +20,7 @@ from simcore_postgres_database.webserver_models import ProjectType, projects from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.sql import select -from sqlalchemy.sql.selectable import Select +from sqlalchemy.sql.selectable import CompoundSelect, Select from ..db.models import GroupType, groups, projects_tags, user_to_groups, users from ..users.exceptions import UserNotFoundError @@ -181,7 +181,7 @@ async def _execute_without_permission_check( conn: SAConnection, user_id: UserID, *, - select_projects_query: Select, + select_projects_query: Select | CompoundSelect, filter_by_services: list[dict] | None = None, ) -> tuple[list[dict[str, Any]], list[ProjectType]]: api_projects: list[dict] = [] # API model-compatible projects diff --git a/services/web/server/src/simcore_service_webserver/projects/exceptions.py b/services/web/server/src/simcore_service_webserver/projects/exceptions.py index 7b8ff61a971..871628ee66d 100644 --- a/services/web/server/src/simcore_service_webserver/projects/exceptions.py +++ b/services/web/server/src/simcore_service_webserver/projects/exceptions.py @@ -30,6 +30,10 @@ class ProjectOwnerNotFoundInTheProjectAccessRightsError(BaseProjectError): msg_template = "Project owner gid with required permissions was not found in the project access rights" +class WrongTagIdsInQueryError(BaseProjectError): + msg_template = "Wrong value in `tag_ids` query parameter" + + class ProjectInvalidRightsError(BaseProjectError): msg_template = ( "User '{user_id}' has no rights to access project with uuid '{project_uuid}'" From 411de87aab8d11505cbf5850c8415403689dcc6a Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 2 Oct 2024 17:05:23 +0200 Subject: [PATCH 08/12] fix --- .../simcore_service_webserver/projects/_crud_handlers.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py index 7db0a59e5a2..a631e3e627d 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py @@ -58,7 +58,7 @@ ProjectActiveParams, ProjectCreateHeaders, ProjectCreateParams, - ProjectListFullSearchParams, + ProjectListFullSearchWithJsonStrParams, ProjectListWithJsonStrParams, ) from ._permalink_api import update_or_pop_permalink_in_project @@ -237,8 +237,10 @@ async def list_projects(request: web.Request): @_handle_projects_exceptions async def list_projects_full_search(request: web.Request): req_ctx = RequestContext.parse_obj(request) - query_params: ProjectListFullSearchParams = parse_request_query_parameters_as( - ProjectListFullSearchParams, request + query_params: ProjectListFullSearchWithJsonStrParams = ( + parse_request_query_parameters_as( + ProjectListFullSearchWithJsonStrParams, request + ) ) if query_params.tag_ids: try: From 681c88f0b96c2779bccad39b0788c8dfff02255f Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 2 Oct 2024 17:28:50 +0200 Subject: [PATCH 09/12] adding unit tests --- .../projects/_crud_handlers.py | 2 +- ...t_workspaces__list_projects_full_search.py | 74 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py index a631e3e627d..92791f3df0b 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py @@ -246,7 +246,7 @@ async def list_projects_full_search(request: web.Request): try: tag_ids_list = list(map(int, query_params.tag_ids.split(","))) parse_obj_as(list[int], tag_ids_list) - except ValidationError as exc: + except (ValidationError, ValueError) as exc: raise WrongTagIdsInQueryError from exc else: tag_ids_list = [] diff --git a/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces__list_projects_full_search.py b/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces__list_projects_full_search.py index 783cc83f489..a7efd64b485 100644 --- a/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces__list_projects_full_search.py +++ b/services/web/server/tests/unit/with_dbs/03/workspaces/test_workspaces__list_projects_full_search.py @@ -5,6 +5,7 @@ # pylint: disable=too-many-statements +import json from copy import deepcopy from http import HTTPStatus @@ -162,3 +163,76 @@ async def test_workspaces__list_projects_full_search( assert sorted_data[2]["uuid"] == project_3["uuid"] assert sorted_data[2]["workspaceId"] is None assert sorted_data[2]["folderId"] == root_folder["folderId"] + + +@pytest.mark.parametrize("user_role,expected", [(UserRole.USER, status.HTTP_200_OK)]) +async def test__list_projects_full_search_with_query_parameters( + client: TestClient, + logged_user: UserInfoDict, + user_project: ProjectDict, + expected: HTTPStatus, + mock_catalog_api_get_services_for_user_in_product: MockerFixture, + fake_project: ProjectDict, + workspaces_clean_db: None, +): + assert client.app + + # Create projects in private workspace + project_data = deepcopy(fake_project) + project_data["name"] = _SEARCH_NAME_2 + project = await create_project( + client.app, + project_data, + user_id=logged_user["id"], + product_name="osparc", + ) + + # Full search with text + base_url = client.app.router["list_projects_full_search"].url_for() + url = base_url.with_query({"text": "Orion"}) + resp = await client.get(url) + data, _ = await assert_status(resp, status.HTTP_200_OK) + assert len(data) == 1 + assert data[0]["uuid"] == project["uuid"] + + # Full search with order_by + base_url = client.app.router["list_projects_full_search"].url_for() + url = base_url.with_query( + { + "text": "Orion", + "order_by": json.dumps({"field": "uuid", "direction": "desc"}), + } + ) + resp = await client.get(url) + data, _ = await assert_status(resp, status.HTTP_200_OK) + assert len(data) == 1 + assert data[0]["uuid"] == project["uuid"] + + # Full search with tag_ids + base_url = client.app.router["list_projects_full_search"].url_for() + url = base_url.with_query({"text": "Orion", "tag_ids": "1,2"}) + resp = await client.get(url) + data, _ = await assert_status(resp, status.HTTP_200_OK) + assert len(data) == 0 + + # Create tag + url = client.app.router["create_tag"].url_for() + resp = await client.post( + f"{url}", json={"name": "tag1", "description": "description1", "color": "#f00"} + ) + added_tag, _ = await assert_status(resp, expected) + + # Add tag to study + url = client.app.router["add_project_tag"].url_for( + project_uuid=project["uuid"], tag_id=str(added_tag.get("id")) + ) + resp = await client.post(f"{url}") + data, _ = await assert_status(resp, expected) + + # Full search with tag_ids + base_url = client.app.router["list_projects_full_search"].url_for() + url = base_url.with_query({"text": "Orion", "tag_ids": f"{added_tag['id']}"}) + resp = await client.get(url) + data, _ = await assert_status(resp, status.HTTP_200_OK) + assert len(data) == 1 + assert data[0]["uuid"] == project["uuid"] From b8e253ae14216eb73bd0b83fd9963abca65e743f Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 2 Oct 2024 17:34:03 +0200 Subject: [PATCH 10/12] migration --- ...6057a0f693_make_project_owner_mandatory.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 packages/postgres-database/src/simcore_postgres_database/migration/versions/d26057a0f693_make_project_owner_mandatory.py diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/d26057a0f693_make_project_owner_mandatory.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/d26057a0f693_make_project_owner_mandatory.py new file mode 100644 index 00000000000..930fb470f22 --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/d26057a0f693_make_project_owner_mandatory.py @@ -0,0 +1,26 @@ +"""make project owner mandatory + +Revision ID: d26057a0f693 +Revises: 10729e07000d +Create Date: 2024-10-02 15:30:20.377698+00:00 + +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = "d26057a0f693" +down_revision = "10729e07000d" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column("projects", "prj_owner", nullable=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column("projects", "prj_owner", nullable=True) + # ### end Alembic commands ### From 0a948956a8b7387aeff6dec960dbff1672945da0 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Thu, 3 Oct 2024 14:42:07 +0200 Subject: [PATCH 11/12] review @pcrespov --- .../servicelib/aiohttp/requests_validation.py | 13 +++------- .../projects/_crud_handlers.py | 11 ++------ .../projects/_crud_handlers_models.py | 25 +++++++++++++++++-- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/packages/service-library/src/servicelib/aiohttp/requests_validation.py b/packages/service-library/src/servicelib/aiohttp/requests_validation.py index a1d6a088878..085243c5d26 100644 --- a/packages/service-library/src/servicelib/aiohttp/requests_validation.py +++ b/packages/service-library/src/servicelib/aiohttp/requests_validation.py @@ -8,7 +8,6 @@ """ import json.decoder -from collections import defaultdict from collections.abc import Iterator from contextlib import contextmanager from typing import TypeAlias, TypeVar, Union @@ -167,15 +166,9 @@ def parse_request_query_parameters_as( resource_name=request.rel_url.path, use_error_v1=use_enveloped_error_v1, ): - tmp_data = defaultdict(list) - for key, value in request.query.items(): - tmp_data[key].append(value) - # Convert defaultdict to a normal dictionary - # And if a key has only one value, store it as that value instead of a list - data = { - key: value if len(value) > 1 else value[0] - for key, value in tmp_data.items() - } + # NOTE: Currently, this does not take into consideration cases where there are multiple + # query parameters with the same key. However, we are not using such cases anywhere at the moment. + data = dict(request.query) if hasattr(parameters_schema_cls, "parse_obj"): return parameters_schema_cls.parse_obj(data) diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py index 92791f3df0b..d2cce731d21 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py @@ -25,7 +25,7 @@ from models_library.rest_pagination_utils import paginate_data from models_library.utils.fastapi_encoders import jsonable_encoder from models_library.utils.json_serialization import json_dumps -from pydantic import ValidationError, parse_obj_as +from pydantic import parse_obj_as from servicelib.aiohttp.long_running_tasks.server import start_long_running_task from servicelib.aiohttp.requests_validation import ( parse_request_body_as, @@ -242,14 +242,7 @@ async def list_projects_full_search(request: web.Request): ProjectListFullSearchWithJsonStrParams, request ) ) - if query_params.tag_ids: - try: - tag_ids_list = list(map(int, query_params.tag_ids.split(","))) - parse_obj_as(list[int], tag_ids_list) - except (ValidationError, ValueError) as exc: - raise WrongTagIdsInQueryError from exc - else: - tag_ids_list = [] + tag_ids_list = query_params.tag_ids_list() projects, total_number_of_projects = await _crud_api_read.list_projects_full_search( request, diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py index 614f2325f1a..2fdef2fb3e2 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py @@ -17,7 +17,15 @@ null_or_none_str_to_none_validator, ) from models_library.workspaces import WorkspaceID -from pydantic import BaseModel, Extra, Field, Json, root_validator, validator +from pydantic import ( + BaseModel, + Extra, + Field, + Json, + parse_obj_as, + root_validator, + validator, +) from servicelib.common_headers import ( UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE, X_SIMCORE_PARENT_NODE_ID, @@ -25,6 +33,7 @@ X_SIMCORE_USER_AGENT, ) +from .exceptions import WrongTagIdsInQueryError from .models import ProjectTypeAPI @@ -180,4 +189,16 @@ class ProjectListFullSearchParams(PageQueryParameters): class ProjectListFullSearchWithJsonStrParams( ProjectListFullSearchParams, ProjectListWithOrderByParams ): - ... + def tag_ids_list(self) -> list[int]: + try: + # Split the tag_ids by commas and map them to integers + if self.tag_ids: + tag_ids_list = list(map(int, self.tag_ids.split(","))) + # Validate that the tag_ids_list is indeed a list of integers + parse_obj_as(list[int], tag_ids_list) + else: + tag_ids_list = [] + except ValueError as exc: + raise WrongTagIdsInQueryError from exc + + return tag_ids_list From 448a7eb4f598989bfbd27bcf40e75f880031bcdb Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Thu, 3 Oct 2024 14:44:25 +0200 Subject: [PATCH 12/12] remove DB changes --- ...6057a0f693_make_project_owner_mandatory.py | 26 ------------------- .../models/projects.py | 1 + 2 files changed, 1 insertion(+), 26 deletions(-) delete mode 100644 packages/postgres-database/src/simcore_postgres_database/migration/versions/d26057a0f693_make_project_owner_mandatory.py diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/d26057a0f693_make_project_owner_mandatory.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/d26057a0f693_make_project_owner_mandatory.py deleted file mode 100644 index 930fb470f22..00000000000 --- a/packages/postgres-database/src/simcore_postgres_database/migration/versions/d26057a0f693_make_project_owner_mandatory.py +++ /dev/null @@ -1,26 +0,0 @@ -"""make project owner mandatory - -Revision ID: d26057a0f693 -Revises: 10729e07000d -Create Date: 2024-10-02 15:30:20.377698+00:00 - -""" -from alembic import op - -# revision identifiers, used by Alembic. -revision = "d26057a0f693" -down_revision = "10729e07000d" -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column("projects", "prj_owner", nullable=False) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column("projects", "prj_owner", nullable=True) - # ### end Alembic commands ### diff --git a/packages/postgres-database/src/simcore_postgres_database/models/projects.py b/packages/postgres-database/src/simcore_postgres_database/models/projects.py index 176d4c7d9fd..ae77ea5c5d0 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/projects.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/projects.py @@ -67,6 +67,7 @@ class ProjectType(enum.Enum): onupdate="CASCADE", ondelete="RESTRICT", ), + nullable=True, doc="Project's owner", index=True, ),