Skip to content

Commit 931595e

Browse files
✨ introduce search parameter to the listing workspaces (#6872)
Co-authored-by: Odei Maiz <[email protected]>
1 parent 0c5a068 commit 931595e

File tree

6 files changed

+102
-5
lines changed

6 files changed

+102
-5
lines changed

services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4540,7 +4540,7 @@ paths:
45404540
'403':
45414541
description: ProjectInvalidRightsError
45424542
'404':
4543-
description: UserDefaultWalletNotFoundError, ProjectNotFoundError
4543+
description: ProjectNotFoundError, UserDefaultWalletNotFoundError
45444544
'409':
45454545
description: ProjectTooManyProjectOpenedError
45464546
'422':

services/web/server/src/simcore_service_webserver/workspaces/_models.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from typing import Annotated
23

34
from models_library.basic_types import IDStr
45
from models_library.rest_base import RequestParameters, StrictRequestParameters
@@ -11,8 +12,9 @@
1112
from models_library.rest_pagination import PageQueryParameters
1213
from models_library.trash import RemoveQueryParams
1314
from models_library.users import GroupID, UserID
15+
from models_library.utils.common_validators import empty_str_to_none_pre_validator
1416
from models_library.workspaces import WorkspaceID
15-
from pydantic import BaseModel, ConfigDict, Field
17+
from pydantic import BaseModel, BeforeValidator, ConfigDict, Field
1618
from servicelib.request_keys import RQT_USERID_KEY
1719

1820
from .._constants import RQ_PRODUCT_KEY
@@ -46,6 +48,14 @@ class WorkspacesFilters(Filters):
4648
default=False,
4749
description="Set to true to list trashed, false to list non-trashed (default), None to list all",
4850
)
51+
text: Annotated[
52+
str | None, BeforeValidator(empty_str_to_none_pre_validator)
53+
] = Field(
54+
default=None,
55+
description="Multi column full text search",
56+
max_length=100,
57+
examples=["My Workspace"],
58+
)
4959

5060

5161
class WorkspacesListQueryParams(

services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ async def list_workspaces(
9191
user_id: UserID,
9292
product_name: ProductName,
9393
filter_trashed: bool | None,
94+
filter_by_text: str | None,
9495
offset: NonNegativeInt,
9596
limit: int,
9697
order_by: OrderBy,
@@ -100,6 +101,7 @@ async def list_workspaces(
100101
user_id=user_id,
101102
product_name=product_name,
102103
filter_trashed=filter_trashed,
104+
filter_by_text=filter_by_text,
103105
offset=offset,
104106
limit=limit,
105107
order_by=order_by,

services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ async def list_workspaces_for_user(
114114
user_id: UserID,
115115
product_name: ProductName,
116116
filter_trashed: bool | None,
117+
filter_by_text: str | None,
117118
offset: NonNegativeInt,
118119
limit: NonNegativeInt,
119120
order_by: OrderBy,
@@ -140,6 +141,11 @@ async def list_workspaces_for_user(
140141
if filter_trashed
141142
else workspaces.c.trashed.is_(None)
142143
)
144+
if filter_by_text is not None:
145+
base_query = base_query.where(
146+
(workspaces.c.name.ilike(f"%{filter_by_text}%"))
147+
| (workspaces.c.description.ilike(f"%{filter_by_text}%"))
148+
)
143149

144150
# Select total count from base_query
145151
subquery = base_query.subquery()

services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ async def list_workspaces(request: web.Request):
7777
user_id=req_ctx.user_id,
7878
product_name=req_ctx.product_name,
7979
filter_trashed=query_params.filters.trashed,
80+
filter_by_text=query_params.filters.text,
8081
offset=query_params.offset,
8182
limit=query_params.limit,
8283
order_by=OrderBy.model_construct(**query_params.order_by.model_dump()),

services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from collections.abc import AsyncIterator
2+
13
# pylint: disable=redefined-outer-name
24
# pylint: disable=unused-argument
35
# pylint: disable=unused-variable
@@ -46,6 +48,7 @@ async def test_workspaces_user_role_permissions(
4648
logged_user: UserInfoDict,
4749
user_project: ProjectDict,
4850
expected: ExpectedResponse,
51+
workspaces_clean_db: AsyncIterator[None],
4952
):
5053
assert client.app
5154

@@ -60,6 +63,7 @@ async def test_workspaces_workflow(
6063
logged_user: UserInfoDict,
6164
user_project: ProjectDict,
6265
expected: HTTPStatus,
66+
workspaces_clean_db: AsyncIterator[None],
6367
):
6468
assert client.app
6569

@@ -139,13 +143,87 @@ async def test_workspaces_workflow(
139143

140144

141145
@pytest.mark.parametrize("user_role,expected", [(UserRole.USER, status.HTTP_200_OK)])
142-
async def test_project_workspace_movement_full_workflow(
146+
async def test_list_workspaces_with_text_search(
143147
client: TestClient,
144148
logged_user: UserInfoDict,
145149
user_project: ProjectDict,
146150
expected: HTTPStatus,
151+
workspaces_clean_db: AsyncIterator[None],
147152
):
148153
assert client.app
149154

150-
# NOTE: MD: not yet implemented
151-
# SEE https://github.com/ITISFoundation/osparc-simcore/issues/6778
155+
# list user workspaces
156+
url = client.app.router["list_workspaces"].url_for()
157+
resp = await client.get(f"{url}")
158+
data, _ = await assert_status(resp, status.HTTP_200_OK)
159+
assert data == []
160+
161+
# CREATE a new workspace
162+
url = client.app.router["create_workspace"].url_for()
163+
resp = await client.post(
164+
f"{url}",
165+
json={
166+
"name": "My first workspace",
167+
"description": "Custom description",
168+
"thumbnail": None,
169+
},
170+
)
171+
data, _ = await assert_status(resp, status.HTTP_201_CREATED)
172+
added_workspace = WorkspaceGet.model_validate(data)
173+
174+
# CREATE a new workspace
175+
url = client.app.router["create_workspace"].url_for()
176+
resp = await client.post(
177+
f"{url}",
178+
json={
179+
"name": "My second workspace",
180+
"description": "Sharing important projects",
181+
"thumbnail": None,
182+
},
183+
)
184+
data, _ = await assert_status(resp, status.HTTP_201_CREATED)
185+
added_workspace = WorkspaceGet.model_validate(data)
186+
187+
# LIST user workspaces
188+
url = client.app.router["list_workspaces"].url_for()
189+
resp = await client.get(f"{url}")
190+
data, _, meta, links = await assert_status(
191+
resp, status.HTTP_200_OK, include_meta=True, include_links=True
192+
)
193+
assert len(data) == 2
194+
195+
# LIST user workspaces
196+
url = (
197+
client.app.router["list_workspaces"]
198+
.url_for()
199+
.with_query({"filters": '{"text": "first"}'})
200+
)
201+
resp = await client.get(f"{url}")
202+
data, _, meta, links = await assert_status(
203+
resp, status.HTTP_200_OK, include_meta=True, include_links=True
204+
)
205+
assert len(data) == 1
206+
207+
# LIST user workspaces
208+
url = (
209+
client.app.router["list_workspaces"]
210+
.url_for()
211+
.with_query({"filters": '{"text": "important"}'})
212+
)
213+
resp = await client.get(f"{url}")
214+
data, _, meta, links = await assert_status(
215+
resp, status.HTTP_200_OK, include_meta=True, include_links=True
216+
)
217+
assert len(data) == 1
218+
219+
# LIST user workspaces
220+
url = (
221+
client.app.router["list_workspaces"]
222+
.url_for()
223+
.with_query({"filters": '{"text": "non-existing"}'})
224+
)
225+
resp = await client.get(f"{url}")
226+
data, _, meta, links = await assert_status(
227+
resp, status.HTTP_200_OK, include_meta=True, include_links=True
228+
)
229+
assert len(data) == 0

0 commit comments

Comments
 (0)