From 931595e47b0a0f37664e8de98102fe69cbd62751 Mon Sep 17 00:00:00 2001 From: Matus Drobuliak <60785969+matusdrobuliak66@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:21:48 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20introduce=20search=20parameter=20to?= =?UTF-8?q?=20the=20listing=20workspaces=20(#6872)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Odei Maiz <33152403+odeimaiz@users.noreply.github.com> --- .../api/v0/openapi.yaml | 2 +- .../workspaces/_models.py | 12 ++- .../workspaces/_workspaces_api.py | 2 + .../workspaces/_workspaces_db.py | 6 ++ .../workspaces/_workspaces_handlers.py | 1 + .../with_dbs/04/workspaces/test_workspaces.py | 84 ++++++++++++++++++- 6 files changed, 102 insertions(+), 5 deletions(-) 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 33979c6bf3d..9cca4bafd06 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 @@ -4540,7 +4540,7 @@ paths: '403': description: ProjectInvalidRightsError '404': - description: UserDefaultWalletNotFoundError, ProjectNotFoundError + description: ProjectNotFoundError, UserDefaultWalletNotFoundError '409': description: ProjectTooManyProjectOpenedError '422': diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_models.py b/services/web/server/src/simcore_service_webserver/workspaces/_models.py index 362d68884c7..af35fe4b63f 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_models.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_models.py @@ -1,4 +1,5 @@ import logging +from typing import Annotated from models_library.basic_types import IDStr from models_library.rest_base import RequestParameters, StrictRequestParameters @@ -11,8 +12,9 @@ from models_library.rest_pagination import PageQueryParameters from models_library.trash import RemoveQueryParams from models_library.users import GroupID, UserID +from models_library.utils.common_validators import empty_str_to_none_pre_validator from models_library.workspaces import WorkspaceID -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, BeforeValidator, ConfigDict, Field from servicelib.request_keys import RQT_USERID_KEY from .._constants import RQ_PRODUCT_KEY @@ -46,6 +48,14 @@ class WorkspacesFilters(Filters): default=False, description="Set to true to list trashed, false to list non-trashed (default), None to list all", ) + text: Annotated[ + str | None, BeforeValidator(empty_str_to_none_pre_validator) + ] = Field( + default=None, + description="Multi column full text search", + max_length=100, + examples=["My Workspace"], + ) class WorkspacesListQueryParams( diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py index 3fd6633bb06..b4881c2816c 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py @@ -91,6 +91,7 @@ async def list_workspaces( user_id: UserID, product_name: ProductName, filter_trashed: bool | None, + filter_by_text: str | None, offset: NonNegativeInt, limit: int, order_by: OrderBy, @@ -100,6 +101,7 @@ async def list_workspaces( user_id=user_id, product_name=product_name, filter_trashed=filter_trashed, + filter_by_text=filter_by_text, offset=offset, limit=limit, order_by=order_by, diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py index 7c55e0a9428..3835e82f9e0 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py @@ -114,6 +114,7 @@ async def list_workspaces_for_user( user_id: UserID, product_name: ProductName, filter_trashed: bool | None, + filter_by_text: str | None, offset: NonNegativeInt, limit: NonNegativeInt, order_by: OrderBy, @@ -140,6 +141,11 @@ async def list_workspaces_for_user( if filter_trashed else workspaces.c.trashed.is_(None) ) + if filter_by_text is not None: + base_query = base_query.where( + (workspaces.c.name.ilike(f"%{filter_by_text}%")) + | (workspaces.c.description.ilike(f"%{filter_by_text}%")) + ) # Select total count from base_query subquery = base_query.subquery() diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py index 9889e286dda..c1f706f259a 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py @@ -77,6 +77,7 @@ async def list_workspaces(request: web.Request): user_id=req_ctx.user_id, product_name=req_ctx.product_name, filter_trashed=query_params.filters.trashed, + filter_by_text=query_params.filters.text, offset=query_params.offset, limit=query_params.limit, order_by=OrderBy.model_construct(**query_params.order_by.model_dump()), diff --git a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py index 5e7d81afd2b..362eca1d82b 100644 --- a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py +++ b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py @@ -1,3 +1,5 @@ +from collections.abc import AsyncIterator + # pylint: disable=redefined-outer-name # pylint: disable=unused-argument # pylint: disable=unused-variable @@ -46,6 +48,7 @@ async def test_workspaces_user_role_permissions( logged_user: UserInfoDict, user_project: ProjectDict, expected: ExpectedResponse, + workspaces_clean_db: AsyncIterator[None], ): assert client.app @@ -60,6 +63,7 @@ async def test_workspaces_workflow( logged_user: UserInfoDict, user_project: ProjectDict, expected: HTTPStatus, + workspaces_clean_db: AsyncIterator[None], ): assert client.app @@ -139,13 +143,87 @@ async def test_workspaces_workflow( @pytest.mark.parametrize("user_role,expected", [(UserRole.USER, status.HTTP_200_OK)]) -async def test_project_workspace_movement_full_workflow( +async def test_list_workspaces_with_text_search( client: TestClient, logged_user: UserInfoDict, user_project: ProjectDict, expected: HTTPStatus, + workspaces_clean_db: AsyncIterator[None], ): assert client.app - # NOTE: MD: not yet implemented - # SEE https://github.com/ITISFoundation/osparc-simcore/issues/6778 + # list user workspaces + url = client.app.router["list_workspaces"].url_for() + resp = await client.get(f"{url}") + data, _ = await assert_status(resp, status.HTTP_200_OK) + assert data == [] + + # CREATE a new workspace + url = client.app.router["create_workspace"].url_for() + resp = await client.post( + f"{url}", + json={ + "name": "My first workspace", + "description": "Custom description", + "thumbnail": None, + }, + ) + data, _ = await assert_status(resp, status.HTTP_201_CREATED) + added_workspace = WorkspaceGet.model_validate(data) + + # CREATE a new workspace + url = client.app.router["create_workspace"].url_for() + resp = await client.post( + f"{url}", + json={ + "name": "My second workspace", + "description": "Sharing important projects", + "thumbnail": None, + }, + ) + data, _ = await assert_status(resp, status.HTTP_201_CREATED) + added_workspace = WorkspaceGet.model_validate(data) + + # LIST user workspaces + url = client.app.router["list_workspaces"].url_for() + resp = await client.get(f"{url}") + data, _, meta, links = await assert_status( + resp, status.HTTP_200_OK, include_meta=True, include_links=True + ) + assert len(data) == 2 + + # LIST user workspaces + url = ( + client.app.router["list_workspaces"] + .url_for() + .with_query({"filters": '{"text": "first"}'}) + ) + resp = await client.get(f"{url}") + data, _, meta, links = await assert_status( + resp, status.HTTP_200_OK, include_meta=True, include_links=True + ) + assert len(data) == 1 + + # LIST user workspaces + url = ( + client.app.router["list_workspaces"] + .url_for() + .with_query({"filters": '{"text": "important"}'}) + ) + resp = await client.get(f"{url}") + data, _, meta, links = await assert_status( + resp, status.HTTP_200_OK, include_meta=True, include_links=True + ) + assert len(data) == 1 + + # LIST user workspaces + url = ( + client.app.router["list_workspaces"] + .url_for() + .with_query({"filters": '{"text": "non-existing"}'}) + ) + resp = await client.get(f"{url}") + data, _, meta, links = await assert_status( + resp, status.HTTP_200_OK, include_meta=True, include_links=True + ) + assert len(data) == 0