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

✨ introduce search parameter to the listing workspaces #6872

Merged
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
Expand Up @@ -4540,7 +4540,7 @@ paths:
'403':
description: ProjectInvalidRightsError
'404':
description: UserDefaultWalletNotFoundError, ProjectNotFoundError
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved
description: ProjectNotFoundError, UserDefaultWalletNotFoundError
'409':
description: ProjectTooManyProjectOpenedError
'422':
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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",
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved
max_length=100,
examples=["My Workspace"],
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved
)


class WorkspacesListQueryParams(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from collections.abc import AsyncIterator

# pylint: disable=redefined-outer-name
# pylint: disable=unused-argument
# pylint: disable=unused-variable
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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
Loading