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

🎨 improve project full search #6483

Merged
9 changes: 8 additions & 1 deletion api/specs/web-server/_projects_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}',),
):
...

Expand Down
Original file line number Diff line number Diff line change
@@ -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 ###
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ class ProjectType(enum.Enum):
onupdate="CASCADE",
ondelete="RESTRICT",
),
nullable=True,
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved
doc="Project's owner",
index=True,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,25 +143,54 @@ 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_ids_list: list[int],
) -> tuple[list[ProjectDict], int]:
db = ProjectDBAPI.get_from_app_context(app)
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_list=tag_ids_list,
)

return db_projects, total_number_projects
projects: list[ProjectDict] = await logged_gather(
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved
*(
_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 projects, total_number_projects


async def get_project(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -58,7 +58,7 @@
ProjectActiveParams,
ProjectCreateHeaders,
ProjectCreateParams,
ProjectListFullSearchParams,
ProjectListFullSearchWithJsonStrParams,
ProjectListWithJsonStrParams,
)
from ._permalink_api import update_or_pop_permalink_in_project
Expand All @@ -69,6 +69,7 @@
ProjectInvalidUsageError,
ProjectNotFoundError,
ProjectOwnerNotFoundInTheProjectAccessRightsError,
WrongTagIdsInQueryError,
)
from .lock import get_project_locked_state
from .models import ProjectDict
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -233,17 +237,29 @@ 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:
tag_ids_list = list(map(int, query_params.tag_ids.split(",")))
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved
parse_obj_as(list[int], tag_ids_list)
except (ValidationError, ValueError) as exc:
raise WrongTagIdsInQueryError from exc
else:
tag_ids_list = []

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_ids_list=tag_ids_list,
)

page = Page[ProjectDict].parse_obj(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -151,6 +151,10 @@ class Config:
extra = Extra.forbid


class ProjectListWithJsonStrParams(ProjectListParams, ProjectListWithOrderByParams):
...


class ProjectActiveParams(BaseModel):
client_session_id: str

Expand All @@ -162,7 +166,18 @@ class ProjectListFullSearchParams(PageQueryParameters):
max_length=100,
example="My Project",
)
tag_ids: str | None = Field(
default=None,
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)(
empty_str_to_none_pre_validator
)


class ProjectListFullSearchWithJsonStrParams(
ProjectListFullSearchParams, ProjectListWithOrderByParams
):
...
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading