Skip to content

Commit e2aeff5

Browse files
🐛 Add project name search parameter for project listing for the API server usecase. (#7066)
1 parent 21561a6 commit e2aeff5

File tree

6 files changed

+63
-24
lines changed

6 files changed

+63
-24
lines changed

services/api-server/src/simcore_service_api_server/services_http/webserver.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# pylint: disable=too-many-public-methods
22

3+
import json
34
import logging
45
import urllib.parse
56
from collections.abc import Mapping
@@ -185,14 +186,16 @@ async def _page_projects(
185186
limit: int,
186187
offset: int,
187188
show_hidden: bool,
188-
search: str | None = None,
189+
search_by_project_name: str | None = None,
189190
) -> Page[ProjectGet]:
190191
assert 1 <= limit <= MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE # nosec
191192
assert offset >= 0 # nosec
192193

193194
optional: dict[str, Any] = {}
194-
if search is not None:
195-
optional["search"] = search
195+
if search_by_project_name is not None:
196+
filters_dict = {"search_by_project_name": search_by_project_name}
197+
filters_json = json.dumps(filters_dict)
198+
optional["filters"] = filters_json
196199

197200
with service_exception_handler(
198201
service_name="Webserver",
@@ -353,9 +356,7 @@ async def get_projects_w_solver_page(
353356
limit=limit,
354357
offset=offset,
355358
show_hidden=True,
356-
# WARNING: better way to match jobs with projects (Next PR if this works fine!)
357-
# WARNING: search text has a limit that I needed to increase for the example!
358-
search=solver_name,
359+
search_by_project_name=solver_name,
359360
)
360361

361362
async def get_projects_page(self, *, limit: int, offset: int):

services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,14 @@ async def list_projects( # pylint: disable=too-many-arguments
6060
project_type: ProjectTypeAPI,
6161
show_hidden: bool,
6262
trashed: bool | None,
63+
# search
64+
search_by_multi_columns: str | None = None,
65+
search_by_project_name: str | None = None,
6366
# pagination
6467
offset: NonNegativeInt,
6568
limit: int,
69+
# ordering
6670
order_by: OrderBy,
67-
# search
68-
search: str | None,
6971
) -> tuple[list[ProjectDict], int]:
7072
app = request.app
7173
db = ProjectDBAPI.get_from_app_context(app)
@@ -116,7 +118,8 @@ async def list_projects( # pylint: disable=too-many-arguments
116118
filter_trashed=trashed,
117119
filter_hidden=show_hidden,
118120
# composed attrs
119-
filter_by_text=search,
121+
search_by_multi_columns=search_by_multi_columns,
122+
search_by_project_name=search_by_project_name,
120123
# pagination
121124
offset=offset,
122125
limit=limit,
@@ -154,7 +157,8 @@ async def list_projects_full_depth(
154157
limit: int,
155158
order_by: OrderBy,
156159
# search
157-
text: str | None,
160+
search_by_multi_columns: str | None,
161+
search_by_project_name: str | None,
158162
) -> tuple[list[ProjectDict], int]:
159163
db = ProjectDBAPI.get_from_app_context(request.app)
160164

@@ -169,9 +173,10 @@ async def list_projects_full_depth(
169173
folder_query=FolderQuery(folder_scope=FolderScope.ALL),
170174
filter_trashed=trashed,
171175
filter_by_services=user_available_services,
172-
filter_by_text=text,
173176
filter_tag_ids_list=tag_ids_list,
174177
filter_by_project_type=ProjectType.STANDARD,
178+
search_by_multi_columns=search_by_multi_columns,
179+
search_by_project_name=search_by_project_name,
175180
offset=offset,
176181
limit=limit,
177182
order_by=order_by,

services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -203,12 +203,13 @@ async def list_projects(request: web.Request):
203203
project_type=query_params.project_type,
204204
show_hidden=query_params.show_hidden,
205205
trashed=query_params.filters.trashed,
206-
limit=query_params.limit,
207-
offset=query_params.offset,
208-
search=query_params.search,
209-
order_by=OrderBy.model_construct(**query_params.order_by.model_dump()),
210206
folder_id=query_params.folder_id,
211207
workspace_id=query_params.workspace_id,
208+
search_by_multi_columns=query_params.search,
209+
search_by_project_name=query_params.filters.search_by_project_name,
210+
offset=query_params.offset,
211+
limit=query_params.limit,
212+
order_by=OrderBy.model_construct(**query_params.order_by.model_dump()),
212213
)
213214

214215
page = Page[ProjectDict].model_validate(
@@ -246,10 +247,11 @@ async def list_projects_full_search(request: web.Request):
246247
product_name=req_ctx.product_name,
247248
trashed=query_params.filters.trashed,
248249
tag_ids_list=tag_ids_list,
250+
search_by_multi_columns=query_params.text,
251+
search_by_project_name=query_params.filters.search_by_project_name,
249252
offset=query_params.offset,
250253
limit=query_params.limit,
251254
order_by=OrderBy.model_construct(**query_params.order_by.model_dump()),
252-
text=query_params.text,
253255
)
254256

255257
page = Page[ProjectDict].model_validate(

services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ class ProjectFilters(Filters):
9898
default=False,
9999
description="Set to true to list trashed, false to list non-trashed (default), None to list all",
100100
)
101+
search_by_project_name: str | None = Field(
102+
default=None,
103+
description="A search query to filter projects by their name. This field performs a case-insensitive partial match against the project name field.",
104+
)
101105

102106

103107
ProjectsListOrderParams = create_ordering_query_model_class(

services/web/server/src/simcore_service_webserver/projects/db.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -380,8 +380,10 @@ async def list_projects( # pylint: disable=too-many-arguments,too-many-statemen
380380
filter_published: bool | None = None,
381381
filter_hidden: bool | None = False,
382382
filter_trashed: bool | None = False,
383-
filter_by_text: str | None = None,
384383
filter_tag_ids_list: list[int] | None = None,
384+
# search
385+
search_by_multi_columns: str | None = None,
386+
search_by_project_name: str | None = None,
385387
# pagination
386388
offset: int | None = 0,
387389
limit: int | None = None,
@@ -472,7 +474,7 @@ async def list_projects( # pylint: disable=too-many-arguments,too-many-statemen
472474
& (projects_to_products.c.product_name == product_name)
473475
)
474476
)
475-
if filter_by_text is not None:
477+
if search_by_multi_columns is not None:
476478
private_workspace_query = private_workspace_query.join(
477479
users, users.c.id == projects.c.prj_owner, isouter=True
478480
)
@@ -546,7 +548,7 @@ async def list_projects( # pylint: disable=too-many-arguments,too-many-statemen
546548
== workspace_query.workspace_id # <-- Specific shared workspace
547549
)
548550

549-
if filter_by_text is not None:
551+
if search_by_multi_columns is not None:
550552
# NOTE: fields searched with text include user's email
551553
shared_workspace_query = shared_workspace_query.join(
552554
users, users.c.id == projects.c.prj_owner, isouter=True
@@ -582,12 +584,16 @@ async def list_projects( # pylint: disable=too-many-arguments,too-many-statemen
582584
# not marked as trashed
583585
else projects.c.trashed.is_(None)
584586
)
585-
if filter_by_text is not None:
587+
if search_by_multi_columns is not None:
586588
attributes_filters.append(
587-
(projects.c.name.ilike(f"%{filter_by_text}%"))
588-
| (projects.c.description.ilike(f"%{filter_by_text}%"))
589-
| (projects.c.uuid.ilike(f"%{filter_by_text}%"))
590-
| (users.c.name.ilike(f"%{filter_by_text}%"))
589+
(projects.c.name.ilike(f"%{search_by_multi_columns}%"))
590+
| (projects.c.description.ilike(f"%{search_by_multi_columns}%"))
591+
| (projects.c.uuid.ilike(f"%{search_by_multi_columns}%"))
592+
| (users.c.name.ilike(f"%{search_by_multi_columns}%"))
593+
)
594+
if search_by_project_name is not None:
595+
attributes_filters.append(
596+
projects.c.name.like(f"%{search_by_project_name}%")
591597
)
592598
if filter_tag_ids_list:
593599
attributes_filters.append(

services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__list_with_query_params.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,27 @@ async def test_list_projects_with_search_parameter(
184184
data, 1, 0, 1, "/v0/projects?search=nAmE+5&offset=0&limit=20", 1
185185
)
186186

187+
# Now we will test specific project name search (used by the API server)
188+
query_parameters = {"filters": '{"search_by_project_name": "Yoda"}'}
189+
url = base_url.with_query(**query_parameters)
190+
assert (
191+
f"{url}"
192+
== f"/{api_version_prefix}/projects?filters=%7B%22search_by_project_name%22:+%22Yoda%22%7D"
193+
)
194+
195+
resp = await client.get(f"{url}")
196+
data = await resp.json()
197+
198+
assert resp.status == 200
199+
_assert_response_data(
200+
data,
201+
1,
202+
0,
203+
1,
204+
"/v0/projects?filters=%7B%22search_by_project_name%22:+%22Yoda%22%7D&offset=0&limit=20",
205+
1,
206+
)
207+
187208
# Now we will test part of uuid search
188209
query_parameters = {"search": "2-fe1b-11ed-b038-cdb1"}
189210
url = base_url.with_query(**query_parameters)

0 commit comments

Comments
 (0)