From 19bea5189893ef7b41ce90b8775978daba338c38 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Wed, 16 Jul 2025 14:20:29 +0200 Subject: [PATCH 01/69] feat: add readFunctions when getting permissions --- .../api_schemas_webserver/users.py | 1 + .../functions/_controller/_functions_rest.py | 5 ++++- .../unit/with_dbs/04/functions_rpc/conftest.py | 3 ++- .../test_functions_controller_rest.py | 16 +++++++++++++++- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/users.py b/packages/models-library/src/models_library/api_schemas_webserver/users.py index b81d13ed086..3c9aa3dd9e9 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/users.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/users.py @@ -407,4 +407,5 @@ def from_domain_model(cls, permission: UserPermission) -> Self: class MyFunctionPermissionsGet(OutputSchema): + read_functions: bool write_functions: bool diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 9d263d58136..4d5be7210ef 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -125,5 +125,8 @@ async def list_user_functions_permissions(request: web.Request) -> web.Response: assert function_permissions.user_id == req_ctx.user_id # nosec return envelope_json_response( - MyFunctionPermissionsGet(write_functions=function_permissions.write_functions) + MyFunctionPermissionsGet( + read_functions=function_permissions.read_functions, + write_functions=function_permissions.write_functions, + ) ) diff --git a/services/web/server/tests/unit/with_dbs/04/functions_rpc/conftest.py b/services/web/server/tests/unit/with_dbs/04/functions_rpc/conftest.py index 3649ba419a2..31d4939417a 100644 --- a/services/web/server/tests/unit/with_dbs/04/functions_rpc/conftest.py +++ b/services/web/server/tests/unit/with_dbs/04/functions_rpc/conftest.py @@ -200,6 +200,7 @@ async def logged_user_function_api_access_rights( asyncpg_engine: AsyncEngine, logged_user: UserInfoDict, *, + expected_read_functions: bool, expected_write_functions: bool, ) -> AsyncIterator[dict[str, Any]]: cm = insert_and_get_row_lifespan( @@ -208,7 +209,7 @@ async def logged_user_function_api_access_rights( values={ "group_id": logged_user["primary_gid"], "product_name": FRONTEND_APP_DEFAULT, - "read_functions": True, + "read_functions": expected_read_functions, "write_functions": expected_write_functions, "execute_functions": True, "read_function_jobs": True, diff --git a/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rest.py b/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rest.py index 68bc2837355..77f26b9a1cb 100644 --- a/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rest.py +++ b/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rest.py @@ -115,13 +115,26 @@ async def test_register_get_delete_function( @pytest.mark.parametrize("user_role", [UserRole.USER]) -@pytest.mark.parametrize("expected_write_functions", [True, False]) +@pytest.mark.parametrize( + "expected_read_functions,expected_write_functions", + [ + (True, True), + (True, False), + (False, True), # Weird, but allowed for testing purposes + (False, False), + ], +) async def test_list_user_functions_permissions( client: TestClient, logged_user: UserInfoDict, + expected_read_functions: bool, expected_write_functions: bool, logged_user_function_api_access_rights: dict[str, Any], ): + assert ( + logged_user_function_api_access_rights["read_functions"] + == expected_read_functions + ) assert ( logged_user_function_api_access_rights["write_functions"] == expected_write_functions @@ -133,4 +146,5 @@ async def test_list_user_functions_permissions( assert not error function_permissions = MyFunctionPermissionsGet.model_validate(data) + assert function_permissions.read_functions == expected_read_functions assert function_permissions.write_functions == expected_write_functions From 298bdd96a46ed541533cc703783b0169058984d5 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Wed, 16 Jul 2025 14:45:20 +0200 Subject: [PATCH 02/69] fix: update openapi-spec --- .../server/src/simcore_service_webserver/api/v0/openapi.yaml | 4 ++++ 1 file changed, 4 insertions(+) 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 ce122ca58cc..35b0060c756 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 @@ -13023,11 +13023,15 @@ components: title: MarkerUI MyFunctionPermissionsGet: properties: + readFunctions: + type: boolean + title: Readfunctions writeFunctions: type: boolean title: Writefunctions type: object required: + - readFunctions - writeFunctions title: MyFunctionPermissionsGet MyGroupsGet: From fa40d712ade4d5962f4bbd76a771d57e0d6d5224 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 17 Jul 2025 10:03:55 +0200 Subject: [PATCH 03/69] feat: add update endpoint --- .../functions/_controller/_functions_rest.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 4d5be7210ef..6d8ab6ef4cc 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -82,6 +82,22 @@ async def get_function(request: web.Request) -> web.Response: ) +@routes.put( + "/{VTAG}/functions/{function_id}", + name="update_function", +) +@login_required +@permission_required("function.write") +@handle_rest_requests_exceptions +async def update_function(request: web.Request) -> web.Response: + path_params = parse_request_path_parameters_as(FunctionPathParams, request) + function_id = path_params.function_id + + req_ctx = AuthenticatedRequestContext.model_validate(request) + + raise NotImplementedError + + @routes.delete( f"/{VTAG}/functions/{{function_id}}", name="delete_function", From 9af987e96e6f4d8113cfc05c5877389b5384f8c9 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 17 Jul 2025 11:22:20 +0200 Subject: [PATCH 04/69] feat: add update functions endpoint --- .../api_schemas_webserver/functions.py | 4 ++ .../src/models_library/functions.py | 5 ++ .../functions/_controller/_functions_rest.py | 20 +++++++- .../functions/_controller/_functions_rpc.py | 9 ++-- .../functions/_functions_repository.py | 47 ++----------------- .../functions/_functions_service.py | 27 ++--------- 6 files changed, 42 insertions(+), 70 deletions(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/functions.py b/packages/models-library/src/models_library/api_schemas_webserver/functions.py index c1f4a7b55e5..f8d022d330b 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/functions.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/functions.py @@ -23,6 +23,7 @@ FunctionOutputs, FunctionOutputSchema, FunctionSchemaClass, + FunctionUpdate, JSONFunctionInputSchema, JSONFunctionOutputSchema, ProjectFunction, @@ -131,3 +132,6 @@ class ProjectFunctionToRegister(ProjectFunction, InputSchema): ... RegisteredProjectFunctionGet | RegisteredSolverFunctionGet, Field(discriminator="function_class"), ] + + +class RegisteredFunctionUpdate(FunctionUpdate, InputSchema): ... diff --git a/packages/models-library/src/models_library/functions.py b/packages/models-library/src/models_library/functions.py index 4ab6fe389b4..966d5a9b974 100644 --- a/packages/models-library/src/models_library/functions.py +++ b/packages/models-library/src/models_library/functions.py @@ -101,6 +101,11 @@ class RegisteredFunctionBase(FunctionBase): created_at: datetime.datetime +class FunctionUpdate(BaseModel): + title: str | None = None + description: str | None = None + + class ProjectFunction(FunctionBase): function_class: Literal[FunctionClass.PROJECT] = FunctionClass.PROJECT project_id: ProjectID diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 6d8ab6ef4cc..7bd8ba821e3 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -4,6 +4,7 @@ FunctionToRegister, RegisteredFunction, RegisteredFunctionGet, + RegisteredFunctionUpdate, ) from models_library.api_schemas_webserver.users import MyFunctionPermissionsGet from pydantic import TypeAdapter @@ -82,7 +83,7 @@ async def get_function(request: web.Request) -> web.Response: ) -@routes.put( +@routes.patch( "/{VTAG}/functions/{function_id}", name="update_function", ) @@ -93,9 +94,24 @@ async def update_function(request: web.Request) -> web.Response: path_params = parse_request_path_parameters_as(FunctionPathParams, request) function_id = path_params.function_id + function_update = TypeAdapter(RegisteredFunctionUpdate).validate_python( + await request.json() + ) req_ctx = AuthenticatedRequestContext.model_validate(request) - raise NotImplementedError + updated_function = await _functions_service.update_function( + request.app, + user_id=req_ctx.user_id, + product_name=req_ctx.product_name, + function_id=function_id, + function=function_update, + ) + + return envelope_json_response( + TypeAdapter(RegisteredFunctionUpdate).validate_python( + updated_function.model_dump(mode="json") + ) + ) @routes.delete( diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rpc.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rpc.py index f77b0701115..470c532d03e 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rpc.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rpc.py @@ -11,6 +11,7 @@ FunctionJobCollectionsListFilters, FunctionJobID, FunctionOutputSchema, + FunctionUpdate, FunctionUserApiAccessRights, RegisteredFunction, RegisteredFunctionJob, @@ -318,12 +319,12 @@ async def update_function_title( function_id: FunctionID, title: str, ) -> RegisteredFunction: - return await _functions_service.update_function_title( + return await _functions_service.update_function( app=app, user_id=user_id, product_name=product_name, function_id=function_id, - title=title, + function=FunctionUpdate(title=title), ) @@ -342,12 +343,12 @@ async def update_function_description( function_id: FunctionID, description: str, ) -> RegisteredFunction: - return await _functions_service.update_function_description( + return await _functions_service.update_function( app=app, user_id=user_id, product_name=product_name, function_id=function_id, - description=description, + function=FunctionUpdate(description=description), ) diff --git a/services/web/server/src/simcore_service_webserver/functions/_functions_repository.py b/services/web/server/src/simcore_service_webserver/functions/_functions_repository.py index cec64473248..65fc448f1e3 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_functions_repository.py +++ b/services/web/server/src/simcore_service_webserver/functions/_functions_repository.py @@ -20,6 +20,7 @@ FunctionOutputs, FunctionOutputSchema, FunctionsApiAccessRights, + FunctionUpdate, FunctionUserApiAccessRights, RegisteredFunctionDB, RegisteredFunctionJobCollectionDB, @@ -633,14 +634,14 @@ async def delete_function( ) -async def _update_function_attribute( +async def update_function( app: web.Application, - connection: AsyncConnection | None, + connection: AsyncConnection | None = None, *, user_id: UserID, product_name: ProductName, function_id: FunctionID, - **update_kwargs, + function: FunctionUpdate, ) -> RegisteredFunctionDB: async with transaction_context(get_asyncpg_engine(app), connection) as transaction: await check_user_api_access_rights( @@ -667,7 +668,7 @@ async def _update_function_attribute( result = await transaction.execute( functions_table.update() .where(functions_table.c.uuid == function_id) - .values(**update_kwargs) + .values(**function.model_dump(exclude_none=True, exclude_unset=True)) .returning(*_FUNCTIONS_TABLE_COLS) ) row = result.one_or_none() @@ -678,44 +679,6 @@ async def _update_function_attribute( return RegisteredFunctionDB.model_validate(row) -async def update_function_title( - app: web.Application, - connection: AsyncConnection | None = None, - *, - user_id: UserID, - product_name: ProductName, - function_id: FunctionID, - title: str, -) -> RegisteredFunctionDB: - return await _update_function_attribute( - app, - connection=connection, - user_id=user_id, - product_name=product_name, - function_id=function_id, - title=title, - ) - - -async def update_function_description( - app: web.Application, - connection: AsyncConnection | None = None, - *, - user_id: UserID, - product_name: ProductName, - function_id: FunctionID, - description: str, -) -> RegisteredFunctionDB: - return await _update_function_attribute( - app, - connection=connection, - user_id=user_id, - product_name=product_name, - function_id=function_id, - description=description, - ) - - async def get_function_job( app: web.Application, connection: AsyncConnection | None = None, diff --git a/services/web/server/src/simcore_service_webserver/functions/_functions_service.py b/services/web/server/src/simcore_service_webserver/functions/_functions_service.py index d74dbe52217..75ba208ab8f 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_functions_service.py +++ b/services/web/server/src/simcore_service_webserver/functions/_functions_service.py @@ -14,6 +14,7 @@ FunctionJobDB, FunctionJobID, FunctionOutputSchema, + FunctionUpdate, FunctionUserAccessRights, FunctionUserApiAccessRights, RegisteredFunction, @@ -291,38 +292,20 @@ async def delete_function_job_collection( ) -async def update_function_title( +async def update_function( app: web.Application, *, user_id: UserID, product_name: ProductName, function_id: FunctionID, - title: str, + function: FunctionUpdate, ) -> RegisteredFunction: - updated_function = await _functions_repository.update_function_title( + updated_function = await _functions_repository.update_function( app=app, user_id=user_id, product_name=product_name, function_id=function_id, - title=title, - ) - return _decode_function(updated_function) - - -async def update_function_description( - app: web.Application, - *, - user_id: UserID, - product_name: ProductName, - function_id: FunctionID, - description: str, -) -> RegisteredFunction: - updated_function = await _functions_repository.update_function_description( - app=app, - user_id=user_id, - product_name=product_name, - function_id=function_id, - description=description, + function=function, ) return _decode_function(updated_function) From ee89ede23f14e5308ef33555e4df700c64ced6f5 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 17 Jul 2025 11:31:01 +0200 Subject: [PATCH 05/69] fix: update openapi-spec --- api/specs/web-server/_functions.py | 11 +++++ .../api/v0/openapi.yaml | 40 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/api/specs/web-server/_functions.py b/api/specs/web-server/_functions.py index 72f4c8af48e..e10f725762f 100644 --- a/api/specs/web-server/_functions.py +++ b/api/specs/web-server/_functions.py @@ -11,6 +11,7 @@ from models_library.api_schemas_webserver.functions import ( FunctionToRegister, RegisteredFunctionGet, + RegisteredFunctionUpdate, ) from models_library.generics import Envelope from simcore_service_webserver._meta import API_VTAG @@ -44,6 +45,16 @@ async def get_function( ): ... +@router.patch( + "/functions/{function_id}", + response_model=Envelope[RegisteredFunctionGet], +) +async def update_function( + _body: RegisteredFunctionUpdate, + _path: Annotated[FunctionPathParams, Depends()], +): ... + + @router.delete( "/functions/{function_id}", status_code=status.HTTP_204_NO_CONTENT, 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 35b0060c756..225baa71107 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 @@ -3395,6 +3395,32 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_Annotated_Union_RegisteredProjectFunctionGet__RegisteredSolverFunctionGet___FieldInfo_annotation_NoneType__required_True__discriminator__function_class____' + patch: + tags: + - functions + summary: Update Function + operationId: update_function + parameters: + - name: function_id + in: path + required: true + schema: + type: string + format: uuid + title: Function Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RegisteredFunctionUpdate' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/Envelope_Annotated_Union_RegisteredProjectFunctionGet__RegisteredSolverFunctionGet___FieldInfo_annotation_NoneType__required_True__discriminator__function_class____' delete: tags: - functions @@ -15978,6 +16004,20 @@ components: - name - message title: RegisterPhoneNextPage + RegisteredFunctionUpdate: + properties: + title: + anyOf: + - type: string + - type: 'null' + title: Title + description: + anyOf: + - type: string + - type: 'null' + title: Description + type: object + title: RegisteredFunctionUpdate RegisteredProjectFunctionGet: properties: functionClass: From 80535cc879ce0f47d9eb1e2aca05b97d2cf8aa20 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 17 Jul 2025 13:20:09 +0200 Subject: [PATCH 06/69] fix: path --- .../functions/_controller/_functions_rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 7bd8ba821e3..e033777501a 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -84,7 +84,7 @@ async def get_function(request: web.Request) -> web.Response: @routes.patch( - "/{VTAG}/functions/{function_id}", + f"/{VTAG}/functions/{{function_id}}", name="update_function", ) @login_required From a5f70663b63e4eb15de85aa1a0bff580ec9f9d18 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 17 Jul 2025 16:23:57 +0200 Subject: [PATCH 07/69] feat: add batch get projects --- .../projects/_projects_repository.py | 20 ++++++++++++++++++- .../projects/_projects_service.py | 9 +++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_projects_repository.py b/services/web/server/src/simcore_service_webserver/projects/_projects_repository.py index 705e4970ee7..98a3c179569 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_projects_repository.py +++ b/services/web/server/src/simcore_service_webserver/projects/_projects_repository.py @@ -8,7 +8,7 @@ from common_library.exclude import Unset, is_set from models_library.basic_types import IDStr from models_library.groups import GroupID -from models_library.projects import ProjectID +from models_library.projects import Project, ProjectID from models_library.rest_ordering import OrderBy, OrderDirection from models_library.rest_pagination import MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE from models_library.workspaces import WorkspaceID @@ -151,6 +151,24 @@ async def batch_get_project_name( return [rows.get(project_uuid) for project_uuid in projects_uuids_str] +async def batch_get_projects( + app: web.Application, + connection: AsyncConnection | None = None, + *, + project_uuids: list[ProjectID], +) -> list[Project]: + if not project_uuids: + return [] + async with pass_or_acquire_connection(get_asyncpg_engine(app), connection) as conn: + query = ( + sql.select(projects) + .select_from(projects) + .where(projects.c.uuid.in_([f"{uuid}" for uuid in project_uuids])) + ) + result = await conn.stream(query) + return [Project.model_validate(row) async for row in result] + + def _select_trashed_by_primary_gid_query() -> sql.Select: return sql.select( projects.c.uuid, diff --git a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py index 6152340916d..716e94294e7 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py +++ b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py @@ -275,6 +275,15 @@ async def batch_get_project_name( return [name if name else "Unknown" for name in get_project_names] +async def batch_get_projects( + app: web.Application, project_uuids: list[ProjectID] +) -> list[Project]: + return await _projects_repository.batch_get_projects( + app=app, + project_uuids=project_uuids, + ) + + # # UPDATE project ----------------------------------------------------- # From 2d87133ed7d7b7c1c4a4583fdef73c044b9b9dd6 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 17 Jul 2025 16:32:50 +0200 Subject: [PATCH 08/69] fix: use new models --- .../projects/_projects_repository.py | 6 +++--- .../simcore_service_webserver/projects/_projects_service.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_projects_repository.py b/services/web/server/src/simcore_service_webserver/projects/_projects_repository.py index 98a3c179569..d368a04b8dc 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_projects_repository.py +++ b/services/web/server/src/simcore_service_webserver/projects/_projects_repository.py @@ -8,7 +8,7 @@ from common_library.exclude import Unset, is_set from models_library.basic_types import IDStr from models_library.groups import GroupID -from models_library.projects import Project, ProjectID +from models_library.projects import ProjectID from models_library.rest_ordering import OrderBy, OrderDirection from models_library.rest_pagination import MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE from models_library.workspaces import WorkspaceID @@ -156,7 +156,7 @@ async def batch_get_projects( connection: AsyncConnection | None = None, *, project_uuids: list[ProjectID], -) -> list[Project]: +) -> list[ProjectDBGet]: if not project_uuids: return [] async with pass_or_acquire_connection(get_asyncpg_engine(app), connection) as conn: @@ -166,7 +166,7 @@ async def batch_get_projects( .where(projects.c.uuid.in_([f"{uuid}" for uuid in project_uuids])) ) result = await conn.stream(query) - return [Project.model_validate(row) async for row in result] + return [ProjectDBGet.model_validate(row) async for row in result] def _select_trashed_by_primary_gid_query() -> sql.Select: diff --git a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py index 716e94294e7..621f7554549 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py +++ b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py @@ -153,7 +153,7 @@ ProjectTooManyProjectOpenedError, ProjectTypeAndTemplateIncompatibilityError, ) -from .models import ProjectDict, ProjectPatchInternalExtended +from .models import ProjectDBGet, ProjectDict, ProjectPatchInternalExtended from .settings import ProjectsSettings, get_plugin_settings from .utils import extract_dns_without_default_port @@ -277,7 +277,7 @@ async def batch_get_project_name( async def batch_get_projects( app: web.Application, project_uuids: list[ProjectID] -) -> list[Project]: +) -> list[ProjectDBGet]: return await _projects_repository.batch_get_projects( app=app, project_uuids=project_uuids, From 14be90359616346c418c094bbf37f70e66a933c2 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 17 Jul 2025 16:34:47 +0200 Subject: [PATCH 09/69] feat: add list endpoint --- api/specs/web-server/_functions.py | 7 +++++++ .../functions/_controller/_functions_rest.py | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/api/specs/web-server/_functions.py b/api/specs/web-server/_functions.py index e10f725762f..15ec04e5aaa 100644 --- a/api/specs/web-server/_functions.py +++ b/api/specs/web-server/_functions.py @@ -36,6 +36,13 @@ async def register_function( ) -> Envelope[RegisteredFunctionGet]: ... +@router.get( + "/functions", + response_model=Envelope[list[RegisteredFunctionGet]], +) +async def list_functions() -> Envelope[list[RegisteredFunctionGet]]: ... + + @router.get( "/functions/{function_id}", response_model=Envelope[RegisteredFunctionGet], diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index e033777501a..60968203a87 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -57,6 +57,17 @@ async def register_function(request: web.Request) -> web.Response: ) +@routes.get( + f"/{VTAG}/functions", + name="list_functions", +) +@login_required +@permission_required("function.read") +@handle_rest_requests_exceptions +async def list_functions(request: web.Request) -> web.Response: + raise NotImplementedError + + @routes.get( f"/{VTAG}/functions/{{function_id}}", name="get_function", @@ -76,6 +87,8 @@ async def get_function(request: web.Request) -> web.Response: product_name=req_ctx.product_name, ) + # TODO: enrich with project data + return envelope_json_response( TypeAdapter(RegisteredFunctionGet).validate_python( registered_function.model_dump(mode="json") From 6043c7d5c8a1536cac9b59e5b6829b65c1120956 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 17 Jul 2025 23:40:49 +0200 Subject: [PATCH 10/69] feat: get function --- .../api_schemas_webserver/functions.py | 4 +- .../api/v0/openapi.yaml | 46 +++++++++++++++++++ .../functions/_controller/_functions_rest.py | 23 +++++++++- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/functions.py b/packages/models-library/src/models_library/api_schemas_webserver/functions.py index f8d022d330b..963f82c78e9 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/functions.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/functions.py @@ -114,7 +114,9 @@ class RegisteredSolverFunctionGet(RegisteredSolverFunction, OutputSchema): ... -class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): ... +class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): + thumbnail: str | None = None + template_id: int | None = None class SolverFunctionToRegister(SolverFunction, InputSchema): ... 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 225baa71107..5bb030b36b9 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 @@ -3348,6 +3348,18 @@ paths: $ref: '#/components/schemas/EnvelopedError' description: Service Unavailable /v0/functions: + get: + tags: + - functions + summary: List Functions + operationId: list_functions + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/Envelope_list_Annotated_Union_RegisteredProjectFunctionGet__RegisteredSolverFunctionGet___FieldInfo_annotation_NoneType__required_True__discriminator__function_class_____' post: tags: - functions @@ -11265,6 +11277,30 @@ components: title: Error type: object title: Envelope[dict[str, Any]] + Envelope_list_Annotated_Union_RegisteredProjectFunctionGet__RegisteredSolverFunctionGet___FieldInfo_annotation_NoneType__required_True__discriminator__function_class_____: + properties: + data: + anyOf: + - items: + oneOf: + - $ref: '#/components/schemas/RegisteredProjectFunctionGet' + - $ref: '#/components/schemas/RegisteredSolverFunctionGet' + discriminator: + propertyName: functionClass + mapping: + PROJECT: '#/components/schemas/RegisteredProjectFunctionGet' + SOLVER: '#/components/schemas/RegisteredSolverFunctionGet' + type: array + - type: 'null' + title: Data + error: + anyOf: + - {} + - type: 'null' + title: Error + type: object + title: Envelope[list[Annotated[Union[RegisteredProjectFunctionGet, RegisteredSolverFunctionGet], + FieldInfo(annotation=NoneType, required=True, discriminator='function_class')]]] Envelope_list_Annotated_str__StringConstraints___: properties: data: @@ -16067,6 +16103,16 @@ components: type: string format: uuid title: Projectid + thumbnail: + anyOf: + - type: string + - type: 'null' + title: Thumbnail + templateId: + anyOf: + - type: integer + - type: 'null' + title: Templateid type: object required: - inputSchema diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 60968203a87..f899bc7c47e 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -5,8 +5,10 @@ RegisteredFunction, RegisteredFunctionGet, RegisteredFunctionUpdate, + RegisteredProjectFunctionGet, ) from models_library.api_schemas_webserver.users import MyFunctionPermissionsGet +from models_library.functions import FunctionClass from pydantic import TypeAdapter from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( @@ -87,7 +89,26 @@ async def get_function(request: web.Request) -> web.Response: product_name=req_ctx.product_name, ) - # TODO: enrich with project data + if registered_function.function_class == FunctionClass.PROJECT: + from ...projects import _projects_service + + assert isinstance(registered_function, RegisteredProjectFunctionGet) # nosec + + project_dict = await _projects_service.get_project_for_user( + app=request.app, + project_uuid=f"{registered_function.project_id}", + user_id=req_ctx.user_id, + ) + + return envelope_json_response( + TypeAdapter(RegisteredProjectFunctionGet).validate_python( + registered_function.model_dump(mode="json") + | { + "thumbnail": project_dict.get("thumbnail", None), + "template_id": project_dict.get("project_id", None), + } + ) + ) return envelope_json_response( TypeAdapter(RegisteredFunctionGet).validate_python( From 7e67484a9325793ebf78b98fdc6e5311b7cfba40 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 17 Jul 2025 23:51:59 +0200 Subject: [PATCH 11/69] fix: minor --- api/specs/web-server/_functions.py | 2 +- .../server/src/simcore_service_webserver/api/v0/openapi.yaml | 4 ++-- .../functions/_controller/_functions_rest.py | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/api/specs/web-server/_functions.py b/api/specs/web-server/_functions.py index 15ec04e5aaa..a3516d253c4 100644 --- a/api/specs/web-server/_functions.py +++ b/api/specs/web-server/_functions.py @@ -40,7 +40,7 @@ async def register_function( "/functions", response_model=Envelope[list[RegisteredFunctionGet]], ) -async def list_functions() -> Envelope[list[RegisteredFunctionGet]]: ... +async def list_functions(): ... @router.get( 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 5bb030b36b9..8498bcf21e0 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 @@ -11277,8 +11277,8 @@ components: title: Error type: object title: Envelope[dict[str, Any]] - Envelope_list_Annotated_Union_RegisteredProjectFunctionGet__RegisteredSolverFunctionGet___FieldInfo_annotation_NoneType__required_True__discriminator__function_class_____: - properties: + ? Envelope_list_Annotated_Union_RegisteredProjectFunctionGet__RegisteredSolverFunctionGet___FieldInfo_annotation_NoneType__required_True__discriminator__function_class_____ + : properties: data: anyOf: - items: diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index f899bc7c47e..6c5099f8ccc 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -19,6 +19,7 @@ from ..._meta import API_VTAG as VTAG from ...login.decorators import login_required from ...models import AuthenticatedRequestContext +from ...projects import _projects_service from ...security.decorators import permission_required from ...utils_aiohttp import envelope_json_response from .. import _functions_service @@ -90,8 +91,6 @@ async def get_function(request: web.Request) -> web.Response: ) if registered_function.function_class == FunctionClass.PROJECT: - from ...projects import _projects_service - assert isinstance(registered_function, RegisteredProjectFunctionGet) # nosec project_dict = await _projects_service.get_project_for_user( From bcd0ff717fd724969bdbcefd88859846a510a0b8 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 11:08:22 +0200 Subject: [PATCH 12/69] feat: add function list --- api/specs/web-server/_functions.py | 8 +- .../api/v0/openapi.yaml | 71 ++++++++++++---- .../functions/_controller/_functions_rest.py | 83 ++++++++++++++++++- .../_controller/_functions_rest_schemas.py | 11 +++ .../projects/_projects_service.py | 4 +- 5 files changed, 157 insertions(+), 20 deletions(-) diff --git a/api/specs/web-server/_functions.py b/api/specs/web-server/_functions.py index a3516d253c4..1946f408d86 100644 --- a/api/specs/web-server/_functions.py +++ b/api/specs/web-server/_functions.py @@ -7,6 +7,7 @@ from typing import Annotated +from _common import as_query from fastapi import APIRouter, Depends, status from models_library.api_schemas_webserver.functions import ( FunctionToRegister, @@ -16,7 +17,9 @@ from models_library.generics import Envelope from simcore_service_webserver._meta import API_VTAG from simcore_service_webserver.functions._controller._functions_rest_schemas import ( + FunctionGetQueryParams, FunctionPathParams, + FunctionsListQueryParams, ) router = APIRouter( @@ -40,7 +43,9 @@ async def register_function( "/functions", response_model=Envelope[list[RegisteredFunctionGet]], ) -async def list_functions(): ... +async def list_functions( + _query: Annotated[as_query(FunctionsListQueryParams), Depends()], +): ... @router.get( @@ -49,6 +54,7 @@ async def list_functions(): ... ) async def get_function( _path: Annotated[FunctionPathParams, Depends()], + _query: Annotated[as_query(FunctionGetQueryParams), Depends()], ): ... 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 8498bcf21e0..7f4f5558e46 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 @@ -3348,37 +3348,25 @@ paths: $ref: '#/components/schemas/EnvelopedError' description: Service Unavailable /v0/functions: - get: - tags: - - functions - summary: List Functions - operationId: list_functions - responses: - '200': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/Envelope_list_Annotated_Union_RegisteredProjectFunctionGet__RegisteredSolverFunctionGet___FieldInfo_annotation_NoneType__required_True__discriminator__function_class_____' post: tags: - functions summary: Register Function operationId: register_function requestBody: + required: true content: application/json: schema: oneOf: - $ref: '#/components/schemas/ProjectFunctionToRegister' - $ref: '#/components/schemas/SolverFunctionToRegister' - title: ' Body' discriminator: propertyName: functionClass mapping: PROJECT: '#/components/schemas/ProjectFunctionToRegister' SOLVER: '#/components/schemas/SolverFunctionToRegister' - required: true + title: ' Body' responses: '200': description: Successful Response @@ -3386,6 +3374,40 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_Annotated_Union_RegisteredProjectFunctionGet__RegisteredSolverFunctionGet___FieldInfo_annotation_NoneType__required_True__discriminator__function_class____' + get: + tags: + - functions + summary: List Functions + operationId: list_functions + parameters: + - name: include_extras + in: query + required: false + schema: + type: boolean + default: false + title: Include Extras + - name: limit + in: query + required: false + schema: + type: integer + default: 20 + title: Limit + - name: offset + in: query + required: false + schema: + type: integer + default: 0 + title: Offset + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/Envelope_list_Annotated_Union_RegisteredProjectFunctionGet__RegisteredSolverFunctionGet___FieldInfo_annotation_NoneType__required_True__discriminator__function_class_____' /v0/functions/{function_id}: get: tags: @@ -3400,6 +3422,27 @@ paths: type: string format: uuid title: Function Id + - name: include_extras + in: query + required: false + schema: + type: boolean + default: false + title: Include Extras + - name: limit + in: query + required: false + schema: + type: integer + default: 20 + title: Limit + - name: offset + in: query + required: false + schema: + type: integer + default: 0 + title: Offset responses: '200': description: Successful Response diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 6c5099f8ccc..0ea96ec233e 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -8,12 +8,15 @@ RegisteredProjectFunctionGet, ) from models_library.api_schemas_webserver.users import MyFunctionPermissionsGet -from models_library.functions import FunctionClass +from models_library.functions import FunctionClass, RegisteredProjectFunction +from models_library.rest_pagination import Page +from models_library.rest_pagination_utils import paginate_data from pydantic import TypeAdapter from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( handle_validation_as_http_error, parse_request_path_parameters_as, + parse_request_query_parameters_as, ) from ..._meta import API_VTAG as VTAG @@ -24,7 +27,11 @@ from ...utils_aiohttp import envelope_json_response from .. import _functions_service from ._functions_rest_exceptions import handle_rest_requests_exceptions -from ._functions_rest_schemas import FunctionPathParams +from ._functions_rest_schemas import ( + FunctionGetQueryParams, + FunctionPathParams, + FunctionsListQueryParams, +) routes = web.RouteTableDef() @@ -68,7 +75,68 @@ async def register_function(request: web.Request) -> web.Response: @permission_required("function.read") @handle_rest_requests_exceptions async def list_functions(request: web.Request) -> web.Response: - raise NotImplementedError + query_params: FunctionsListQueryParams = parse_request_query_parameters_as( + FunctionsListQueryParams, request + ) + + req_ctx = AuthenticatedRequestContext.model_validate(request) + functions, page_meta_info = await _functions_service.list_functions( + request.app, + user_id=req_ctx.user_id, + product_name=req_ctx.product_name, + pagination_limit=query_params.limit, + pagination_offset=query_params.offset, + ) + + chunk = [] + + if query_params.include_extras: + project_ids = [] + for function in functions: + if function.function_class == FunctionClass.PROJECT: + assert isinstance(function, RegisteredProjectFunction) + project_ids.append(function.project_id) + + projects = await _projects_service.batch_get_projects( + request.app, + project_uuids=project_ids, + ) + projects_map = {f"{p.uuid}": p for p in projects} + + for function in functions: + if ( + query_params.include_extras + and function.function_class == FunctionClass.PROJECT + ): + assert isinstance(function, RegisteredProjectFunction) # nosec + project = projects_map.get(f"{function.project_id}") + if project: + chunk.append( + TypeAdapter(RegisteredProjectFunctionGet).validate_python( + function.model_dump(mode="json") + | { + "thumbnail": project.thumbnail, + "template_id": project.id, + } + ) + ) + else: + chunk.append( + TypeAdapter(RegisteredFunctionGet).validate_python( + function.model_dump(mode="json") + ) + ) + + page = Page[RegisteredFunctionGet].model_validate( + paginate_data( + chunk=chunk, + request_url=request.url, + total=page_meta_info.total, + limit=query_params.limit, + offset=query_params.offset, + ) + ) + return envelope_json_response(page) @routes.get( @@ -82,6 +150,10 @@ async def get_function(request: web.Request) -> web.Response: path_params = parse_request_path_parameters_as(FunctionPathParams, request) function_id = path_params.function_id + query_params: FunctionGetQueryParams = parse_request_query_parameters_as( + FunctionGetQueryParams, request + ) + req_ctx = AuthenticatedRequestContext.model_validate(request) registered_function: RegisteredFunction = await _functions_service.get_function( app=request.app, @@ -90,7 +162,10 @@ async def get_function(request: web.Request) -> web.Response: product_name=req_ctx.product_name, ) - if registered_function.function_class == FunctionClass.PROJECT: + if ( + query_params.include_extras + and registered_function.function_class == FunctionClass.PROJECT + ): assert isinstance(registered_function, RegisteredProjectFunctionGet) # nosec project_dict = await _projects_service.get_project_for_user( diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py index f97f5728511..fa6147e52e4 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py @@ -1,4 +1,5 @@ from models_library.functions import FunctionID +from models_library.rest_pagination import PageQueryParameters from pydantic import BaseModel, ConfigDict from ...models import AuthenticatedRequestContext @@ -11,4 +12,14 @@ class FunctionPathParams(BaseModel): model_config = ConfigDict(populate_by_name=True, extra="forbid") +class _FunctionQueryParams(BaseModel): + include_extras: bool = False + + +class FunctionGetQueryParams(PageQueryParameters, _FunctionQueryParams): ... + + +class FunctionsListQueryParams(PageQueryParameters, _FunctionQueryParams): ... + + __all__: tuple[str, ...] = ("AuthenticatedRequestContext",) diff --git a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py index 621f7554549..b2b50257b1c 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py +++ b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py @@ -276,7 +276,9 @@ async def batch_get_project_name( async def batch_get_projects( - app: web.Application, project_uuids: list[ProjectID] + app: web.Application, + *, + project_uuids: list[ProjectID], ) -> list[ProjectDBGet]: return await _projects_repository.batch_get_projects( app=app, From 44ad1ce249f2947f4ba83feda72c45e628d9e13e Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 11:46:54 +0200 Subject: [PATCH 13/69] fix: function listing --- .../functions/_controller/_functions_rest.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 0ea96ec233e..602cd4324bf 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -126,6 +126,15 @@ async def list_functions(request: web.Request) -> web.Response: function.model_dump(mode="json") ) ) + else: + chunk.extend( + [ + TypeAdapter(RegisteredFunctionGet).validate_python( + function.model_dump(mode="json") + ) + for function in functions + ] + ) page = Page[RegisteredFunctionGet].model_validate( paginate_data( From 4ce827124bed9ee25525eebbf6c02a4c518a5fe1 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 12:50:36 +0200 Subject: [PATCH 14/69] fix: project id missing --- .../functions/_controller/_functions_rest.py | 56 +++++++++---------- .../_controller/_functions_rest_schemas.py | 2 +- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 602cd4324bf..c6ca081ed71 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -23,6 +23,7 @@ from ...login.decorators import login_required from ...models import AuthenticatedRequestContext from ...projects import _projects_service +from ...projects.models import ProjectDBGet from ...security.decorators import permission_required from ...utils_aiohttp import envelope_json_response from .. import _functions_service @@ -88,7 +89,10 @@ async def list_functions(request: web.Request) -> web.Response: pagination_offset=query_params.offset, ) - chunk = [] + chunk: list[RegisteredFunctionGet] = [] + projects_map: dict[str, ProjectDBGet] = ( + {} + ) # ProjectDBGet has to be renamed at some point! if query_params.include_extras: project_ids = [] @@ -101,40 +105,34 @@ async def list_functions(request: web.Request) -> web.Response: request.app, project_uuids=project_ids, ) - projects_map = {f"{p.uuid}": p for p in projects} - - for function in functions: - if ( - query_params.include_extras - and function.function_class == FunctionClass.PROJECT - ): - assert isinstance(function, RegisteredProjectFunction) # nosec - project = projects_map.get(f"{function.project_id}") - if project: - chunk.append( - TypeAdapter(RegisteredProjectFunctionGet).validate_python( - function.model_dump(mode="json") - | { - "thumbnail": project.thumbnail, - "template_id": project.id, - } - ) - ) - else: + for project in projects: + projects_map[f"{project.uuid}"] = project + + for function in functions: + if ( + query_params.include_extras + and function.function_class == FunctionClass.PROJECT + ): + assert isinstance(function, RegisteredProjectFunction) # nosec + project = projects_map.get(f"{function.project_id}") + if project: chunk.append( - TypeAdapter(RegisteredFunctionGet).validate_python( + TypeAdapter(RegisteredProjectFunctionGet).validate_python( function.model_dump(mode="json") + | { + "thumbnail": ( + f"{project.thumbnail}" if project.thumbnail else None + ), + "template_id": project.id, + } ) ) - else: - chunk.extend( - [ + else: + chunk.append( TypeAdapter(RegisteredFunctionGet).validate_python( function.model_dump(mode="json") ) - for function in functions - ] - ) + ) page = Page[RegisteredFunctionGet].model_validate( paginate_data( @@ -188,7 +186,7 @@ async def get_function(request: web.Request) -> web.Response: registered_function.model_dump(mode="json") | { "thumbnail": project_dict.get("thumbnail", None), - "template_id": project_dict.get("project_id", None), + "template_id": project_dict.get("id", None), } ) ) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py index fa6147e52e4..79030f202be 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py @@ -16,7 +16,7 @@ class _FunctionQueryParams(BaseModel): include_extras: bool = False -class FunctionGetQueryParams(PageQueryParameters, _FunctionQueryParams): ... +class FunctionGetQueryParams(_FunctionQueryParams): ... class FunctionsListQueryParams(PageQueryParameters, _FunctionQueryParams): ... From a65a99be91dee6e3333719510a32655886ab9147 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 13:36:25 +0200 Subject: [PATCH 15/69] fix: list functions pagination --- .../functions/_controller/_functions_rest.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index c6ca081ed71..3ce5dd58bb8 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -9,7 +9,7 @@ ) from models_library.api_schemas_webserver.users import MyFunctionPermissionsGet from models_library.functions import FunctionClass, RegisteredProjectFunction -from models_library.rest_pagination import Page +from models_library.rest_pagination import ItemT, Page from models_library.rest_pagination_utils import paginate_data from pydantic import TypeAdapter from servicelib.aiohttp import status @@ -18,6 +18,8 @@ parse_request_path_parameters_as, parse_request_query_parameters_as, ) +from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON +from servicelib.rest_constants import RESPONSE_MODEL_POLICY from ..._meta import API_VTAG as VTAG from ...login.decorators import login_required @@ -37,6 +39,13 @@ routes = web.RouteTableDef() +def _create_json_response_from_page(page: Page[ItemT]): + return web.Response( + text=page.model_dump_json(**RESPONSE_MODEL_POLICY), + content_type=MIMETYPE_APPLICATION_JSON, + ) + + @routes.post(f"/{VTAG}/functions", name="register_function") @login_required @handle_rest_requests_exceptions @@ -143,7 +152,7 @@ async def list_functions(request: web.Request) -> web.Response: offset=query_params.offset, ) ) - return envelope_json_response(page) + return _create_json_response_from_page(page) @routes.get( From e21a10b809d9f375ca65938a44224a5f89848012 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 13:42:07 +0200 Subject: [PATCH 16/69] fix: enable frontend --- .../client/source/class/osparc/data/Resources.js | 8 ++++++++ .../client/source/class/osparc/store/Functions.js | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/data/Resources.js b/services/static-webserver/client/source/class/osparc/data/Resources.js index dffd67aaac2..e4e2492a0ac 100644 --- a/services/static-webserver/client/source/class/osparc/data/Resources.js +++ b/services/static-webserver/client/source/class/osparc/data/Resources.js @@ -631,6 +631,14 @@ qx.Class.define("osparc.data.Resources", { create: { method: "POST", url: statics.API + "/functions" + }, + getOne: { + method: "GET", + url: statics.API + "/functions/{functionId}?include_extras=true" + }, + getPage: { + method: "GET", + url: statics.API + "/functions?include_extras=true" } } }, diff --git a/services/static-webserver/client/source/class/osparc/store/Functions.js b/services/static-webserver/client/source/class/osparc/store/Functions.js index 487e844c911..7e951e6c327 100644 --- a/services/static-webserver/client/source/class/osparc/store/Functions.js +++ b/services/static-webserver/client/source/class/osparc/store/Functions.js @@ -92,7 +92,7 @@ qx.Class.define("osparc.store.Functions", { }, fetchFunctionsPaginated: function(params, options) { - const isBackendReady = false; + const isBackendReady = true; if (!isBackendReady) { return new Promise(resolve => { const response = this.__dummyResponse(); @@ -110,7 +110,7 @@ qx.Class.define("osparc.store.Functions", { }, fetchFunction: function(functionId) { - const isBackendReady = false; + const isBackendReady = true; if (!isBackendReady) { return new Promise(resolve => { const response = this.__dummyResponse(); From e70e67e7383be259895b3ef3fbd1fc2f09c65ccd Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 13:51:28 +0200 Subject: [PATCH 17/69] fix: openapi spec --- .../api/v0/openapi.yaml | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 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 7f4f5558e46..5f83c872432 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 @@ -3429,20 +3429,6 @@ paths: type: boolean default: false title: Include Extras - - name: limit - in: query - required: false - schema: - type: integer - default: 20 - title: Limit - - name: offset - in: query - required: false - schema: - type: integer - default: 0 - title: Offset responses: '200': description: Successful Response @@ -16146,6 +16132,10 @@ components: type: string format: uuid title: Projectid + uuid: + type: string + format: uuid + title: Uuid thumbnail: anyOf: - type: string @@ -16164,6 +16154,7 @@ components: - uid - createdAt - projectId + - uuid title: RegisteredProjectFunctionGet RegisteredSolverFunctionGet: properties: From 3c462331036c4aec9587cb8c0d80c3e6285bae9d Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 13:51:54 +0200 Subject: [PATCH 18/69] fix: add function ID --- .../src/models_library/api_schemas_webserver/functions.py | 1 + .../functions/_controller/_functions_rest.py | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/functions.py b/packages/models-library/src/models_library/api_schemas_webserver/functions.py index 963f82c78e9..fca93be4701 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/functions.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/functions.py @@ -115,6 +115,7 @@ class RegisteredSolverFunctionGet(RegisteredSolverFunction, OutputSchema): ... class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): + uuid: FunctionID thumbnail: str | None = None template_id: int | None = None diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 3ce5dd58bb8..fc8ce0f46d2 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -129,6 +129,7 @@ async def list_functions(request: web.Request) -> web.Response: TypeAdapter(RegisteredProjectFunctionGet).validate_python( function.model_dump(mode="json") | { + "uuid": function.uid, "thumbnail": ( f"{project.thumbnail}" if project.thumbnail else None ), From d2866549d6008f7b35140f314363081be6f9e06c Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 14:01:49 +0200 Subject: [PATCH 19/69] fix: remap keys --- .../functions/_controller/_functions_rest.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index fc8ce0f46d2..cd373399e5c 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -1,4 +1,5 @@ from aiohttp import web +from common_library.dict_tools import remap_keys from models_library.api_schemas_webserver.functions import ( Function, FunctionToRegister, @@ -127,9 +128,13 @@ async def list_functions(request: web.Request) -> web.Response: if project: chunk.append( TypeAdapter(RegisteredProjectFunctionGet).validate_python( - function.model_dump(mode="json") + remap_keys( + function.model_dump(mode="json"), + rename={ + "uid": "uuid", + }, + ) | { - "uuid": function.uid, "thumbnail": ( f"{project.thumbnail}" if project.thumbnail else None ), @@ -193,7 +198,12 @@ async def get_function(request: web.Request) -> web.Response: return envelope_json_response( TypeAdapter(RegisteredProjectFunctionGet).validate_python( - registered_function.model_dump(mode="json") + remap_keys( + registered_function.model_dump(mode="json"), + rename={ + "uid": "uuid", + }, + ) | { "thumbnail": project_dict.get("thumbnail", None), "template_id": project_dict.get("id", None), From a29729b19dd9c7c810194c7fa70fc94d6f2f1eef Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 14:20:52 +0200 Subject: [PATCH 20/69] fix: id key name --- .../source/class/osparc/data/model/Function.js | 2 +- .../functions/_controller/_functions_rest.py | 15 ++------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/data/model/Function.js b/services/static-webserver/client/source/class/osparc/data/model/Function.js index 58f89f7fc99..35f8b733618 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Function.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Function.js @@ -29,7 +29,7 @@ qx.Class.define("osparc.data.model.Function", { this.base(arguments); this.set({ - uuid: functionData.uuid, + uuid: functionData.uid, functionType: functionData.functionClass, name: functionData.name, description: functionData.description, diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index cd373399e5c..3ce5dd58bb8 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -1,5 +1,4 @@ from aiohttp import web -from common_library.dict_tools import remap_keys from models_library.api_schemas_webserver.functions import ( Function, FunctionToRegister, @@ -128,12 +127,7 @@ async def list_functions(request: web.Request) -> web.Response: if project: chunk.append( TypeAdapter(RegisteredProjectFunctionGet).validate_python( - remap_keys( - function.model_dump(mode="json"), - rename={ - "uid": "uuid", - }, - ) + function.model_dump(mode="json") | { "thumbnail": ( f"{project.thumbnail}" if project.thumbnail else None @@ -198,12 +192,7 @@ async def get_function(request: web.Request) -> web.Response: return envelope_json_response( TypeAdapter(RegisteredProjectFunctionGet).validate_python( - remap_keys( - registered_function.model_dump(mode="json"), - rename={ - "uid": "uuid", - }, - ) + registered_function.model_dump(mode="json") | { "thumbnail": project_dict.get("thumbnail", None), "template_id": project_dict.get("id", None), From fcedfc043cd0574aa822efa117be37622f53155d Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 14:28:50 +0200 Subject: [PATCH 21/69] fix: remove unused uuid --- .../src/models_library/api_schemas_webserver/functions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/functions.py b/packages/models-library/src/models_library/api_schemas_webserver/functions.py index fca93be4701..963f82c78e9 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/functions.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/functions.py @@ -115,7 +115,6 @@ class RegisteredSolverFunctionGet(RegisteredSolverFunction, OutputSchema): ... class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): - uuid: FunctionID thumbnail: str | None = None template_id: int | None = None From 0c506ad7ffab939b401dbbc1229b52506ac75665 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 15:29:07 +0200 Subject: [PATCH 22/69] fix: template ID --- .../src/models_library/api_schemas_webserver/functions.py | 4 ++++ .../client/source/class/osparc/data/model/Function.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/functions.py b/packages/models-library/src/models_library/api_schemas_webserver/functions.py index 963f82c78e9..7814c1ed6ed 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/functions.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/functions.py @@ -47,6 +47,7 @@ UnsupportedFunctionClassError, UnsupportedFunctionFunctionJobClassCombinationError, ) +from ..projects import ProjectID from ._base import InputSchema, OutputSchema __all__ = [ @@ -115,6 +116,9 @@ class RegisteredSolverFunctionGet(RegisteredSolverFunction, OutputSchema): ... class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): + uid: Annotated[FunctionID, Field(alias="uuid")] + title: Annotated[str, Field(alias="name")] = "" + project_id: Annotated[ProjectID, Field(alias="template_id")] thumbnail: str | None = None template_id: int | None = None diff --git a/services/static-webserver/client/source/class/osparc/data/model/Function.js b/services/static-webserver/client/source/class/osparc/data/model/Function.js index 35f8b733618..58f89f7fc99 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Function.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Function.js @@ -29,7 +29,7 @@ qx.Class.define("osparc.data.model.Function", { this.base(arguments); this.set({ - uuid: functionData.uid, + uuid: functionData.uuid, functionType: functionData.functionClass, name: functionData.name, description: functionData.description, From 8cbf75e3e68da1a06ccf31851e614c27107d0a70 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 15:31:10 +0200 Subject: [PATCH 23/69] fix: use template ID --- .../functions/_controller/_functions_rest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 3ce5dd58bb8..5217e6ee041 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -132,7 +132,6 @@ async def list_functions(request: web.Request) -> web.Response: "thumbnail": ( f"{project.thumbnail}" if project.thumbnail else None ), - "template_id": project.id, } ) ) @@ -195,7 +194,6 @@ async def get_function(request: web.Request) -> web.Response: registered_function.model_dump(mode="json") | { "thumbnail": project_dict.get("thumbnail", None), - "template_id": project_dict.get("id", None), } ) ) From f06b3bf1e495c6e315f22dc0e384f6c6c416946a Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 15:47:13 +0200 Subject: [PATCH 24/69] feat: retrieve modified --- packages/models-library/src/models_library/functions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/models-library/src/models_library/functions.py b/packages/models-library/src/models_library/functions.py index 966d5a9b974..da462beb234 100644 --- a/packages/models-library/src/models_library/functions.py +++ b/packages/models-library/src/models_library/functions.py @@ -255,6 +255,7 @@ class FunctionDB(BaseModel): class RegisteredFunctionDB(FunctionDB): uuid: FunctionID created: datetime.datetime + modified: datetime.datetime class FunctionJobCollectionDB(BaseModel): From 48f19aff4c3222c825ee8b204a5ede6de2b63355 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 16:00:45 +0200 Subject: [PATCH 25/69] feat: expose modified at --- .../src/models_library/api_schemas_webserver/functions.py | 7 +++++-- packages/models-library/src/models_library/functions.py | 1 + .../functions/_functions_service.py | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/functions.py b/packages/models-library/src/models_library/api_schemas_webserver/functions.py index 7814c1ed6ed..f196f0ec546 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/functions.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/functions.py @@ -1,3 +1,4 @@ +import datetime from typing import Annotated, TypeAlias from pydantic import Field @@ -118,9 +119,11 @@ class RegisteredSolverFunctionGet(RegisteredSolverFunction, OutputSchema): ... class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): uid: Annotated[FunctionID, Field(alias="uuid")] title: Annotated[str, Field(alias="name")] = "" - project_id: Annotated[ProjectID, Field(alias="template_id")] + project_id: Annotated[ProjectID, Field(alias="templateId")] + created_at: Annotated[datetime.datetime, Field(alias="creationDate")] + modified_at: Annotated[datetime.datetime, Field(alias="lastChangeDate")] thumbnail: str | None = None - template_id: int | None = None + template_id: ProjectID | None = None class SolverFunctionToRegister(SolverFunction, InputSchema): ... diff --git a/packages/models-library/src/models_library/functions.py b/packages/models-library/src/models_library/functions.py index da462beb234..6f1cbaf136f 100644 --- a/packages/models-library/src/models_library/functions.py +++ b/packages/models-library/src/models_library/functions.py @@ -99,6 +99,7 @@ class FunctionBase(BaseModel): class RegisteredFunctionBase(FunctionBase): uid: FunctionID created_at: datetime.datetime + modified_at: datetime.datetime class FunctionUpdate(BaseModel): diff --git a/services/web/server/src/simcore_service_webserver/functions/_functions_service.py b/services/web/server/src/simcore_service_webserver/functions/_functions_service.py index 75ba208ab8f..e23cf0317c3 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_functions_service.py +++ b/services/web/server/src/simcore_service_webserver/functions/_functions_service.py @@ -457,6 +457,7 @@ def _decode_function( project_id=function.class_specific_data["project_id"], default_inputs=function.default_inputs, created_at=function.created, + modified_at=function.modified, ) if function.function_class == FunctionClass.SOLVER: @@ -470,6 +471,7 @@ def _decode_function( solver_version=function.class_specific_data["solver_version"], default_inputs=function.default_inputs, created_at=function.created, + modified_at=function.modified, ) raise UnsupportedFunctionClassError(function_class=function.function_class) From 0a969b51d4d8584a65f6577de642a95ff7682f9b Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 16:09:02 +0200 Subject: [PATCH 26/69] fix: return proper templateId --- .../src/models_library/api_schemas_webserver/functions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/functions.py b/packages/models-library/src/models_library/api_schemas_webserver/functions.py index f196f0ec546..aeadde571e7 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/functions.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/functions.py @@ -123,7 +123,6 @@ class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): created_at: Annotated[datetime.datetime, Field(alias="creationDate")] modified_at: Annotated[datetime.datetime, Field(alias="lastChangeDate")] thumbnail: str | None = None - template_id: ProjectID | None = None class SolverFunctionToRegister(SolverFunction, InputSchema): ... From e8ca8bf33e6131a9436599c04cd6a90581421a80 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 16:36:55 +0200 Subject: [PATCH 27/69] fix: typecheck --- .../functions/_controller/_functions_rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 5217e6ee041..c8a9bfdf328 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -99,7 +99,7 @@ async def list_functions(request: web.Request) -> web.Response: ) chunk: list[RegisteredFunctionGet] = [] - projects_map: dict[str, ProjectDBGet] = ( + projects_map: dict[str, ProjectDBGet | None] = ( {} ) # ProjectDBGet has to be renamed at some point! From 4840afde1f80f4eb1d4e1f57e760ac45ab6a2e55 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Mon, 21 Jul 2025 10:09:09 +0200 Subject: [PATCH 28/69] tests: add list functions --- .../test_functions_controller_rest.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rest.py b/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rest.py index 77f26b9a1cb..5bfcdc8654d 100644 --- a/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rest.py +++ b/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rest.py @@ -18,6 +18,7 @@ RegisteredProjectFunctionGet, ) from models_library.api_schemas_webserver.users import MyFunctionPermissionsGet +from pydantic import TypeAdapter from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.webserver_users import UserInfoDict from servicelib.aiohttp import status @@ -50,13 +51,14 @@ def mock_function() -> dict[str, Any]: @pytest.mark.parametrize( - "user_role,add_user_function_api_access_rights,expected_register,expected_get,expected_delete,expected_get2", + "user_role,add_user_function_api_access_rights,expected_register,expected_get,expected_list,expected_delete,expected_get2", [ ( UserRole.USER, True, status.HTTP_201_CREATED, status.HTTP_200_OK, + status.HTTP_200_OK, status.HTTP_204_NO_CONTENT, status.HTTP_404_NOT_FOUND, ), @@ -67,16 +69,18 @@ def mock_function() -> dict[str, Any]: status.HTTP_403_FORBIDDEN, status.HTTP_403_FORBIDDEN, status.HTTP_403_FORBIDDEN, + status.HTTP_403_FORBIDDEN, ), ], indirect=["add_user_function_api_access_rights"], ) -async def test_register_get_delete_function( +async def test_function_workflow( client: TestClient, logged_user: UserInfoDict, mock_function: dict[str, Any], expected_register: HTTPStatus, expected_get: HTTPStatus, + expected_list: HTTPStatus, expected_delete: HTTPStatus, expected_get2: HTTPStatus, add_user_function_api_access_rights: AsyncIterator[None], @@ -101,6 +105,16 @@ async def test_register_get_delete_function( retrieved_function = RegisteredProjectFunctionGet.model_validate(data) assert retrieved_function.uid == returned_function.uid + url = client.app.router["list_functions"].url_for() + response = await client.get(url) + data, error = await assert_status(response, expected_list) + if not error: + retrieved_functions = TypeAdapter( + list[RegisteredProjectFunctionGet] + ).validate_python(data) + assert len(retrieved_functions) == 1 + assert retrieved_functions[0].uid == returned_function_uid + url = client.app.router["delete_function"].url_for( function_id=str(returned_function_uid) ) From a72715869287c448595cb78357d232eb6854af5d Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Mon, 21 Jul 2025 12:58:33 +0200 Subject: [PATCH 29/69] tests: add function update --- .../functions/_controller/_functions_rest.py | 4 +-- .../security/_authz_access_roles.py | 1 + .../test_functions_controller_rest.py | 31 ++++++++++++++++--- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index c8a9bfdf328..af2865370fb 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -210,7 +210,7 @@ async def get_function(request: web.Request) -> web.Response: name="update_function", ) @login_required -@permission_required("function.write") +@permission_required("function.update") @handle_rest_requests_exceptions async def update_function(request: web.Request) -> web.Response: path_params = parse_request_path_parameters_as(FunctionPathParams, request) @@ -230,7 +230,7 @@ async def update_function(request: web.Request) -> web.Response: ) return envelope_json_response( - TypeAdapter(RegisteredFunctionUpdate).validate_python( + TypeAdapter(RegisteredFunctionGet).validate_python( updated_function.model_dump(mode="json") ) ) diff --git a/services/web/server/src/simcore_service_webserver/security/_authz_access_roles.py b/services/web/server/src/simcore_service_webserver/security/_authz_access_roles.py index 0edd5b2a10c..0c0579a0b69 100644 --- a/services/web/server/src/simcore_service_webserver/security/_authz_access_roles.py +++ b/services/web/server/src/simcore_service_webserver/security/_authz_access_roles.py @@ -78,6 +78,7 @@ class PermissionDict(TypedDict, total=False): "project.workspaces.*", "function.create", "function.read", + "function.update", "function.execute", "function.delete", "resource-usage.read", diff --git a/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rest.py b/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rest.py index 5bfcdc8654d..24e412431eb 100644 --- a/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rest.py +++ b/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rest.py @@ -51,7 +51,7 @@ def mock_function() -> dict[str, Any]: @pytest.mark.parametrize( - "user_role,add_user_function_api_access_rights,expected_register,expected_get,expected_list,expected_delete,expected_get2", + "user_role,add_user_function_api_access_rights,expected_register,expected_get,expected_list,expected_update,expected_delete,expected_get2", [ ( UserRole.USER, @@ -59,6 +59,7 @@ def mock_function() -> dict[str, Any]: status.HTTP_201_CREATED, status.HTTP_200_OK, status.HTTP_200_OK, + status.HTTP_200_OK, status.HTTP_204_NO_CONTENT, status.HTTP_404_NOT_FOUND, ), @@ -70,6 +71,7 @@ def mock_function() -> dict[str, Any]: status.HTTP_403_FORBIDDEN, status.HTTP_403_FORBIDDEN, status.HTTP_403_FORBIDDEN, + status.HTTP_403_FORBIDDEN, ), ], indirect=["add_user_function_api_access_rights"], @@ -81,11 +83,13 @@ async def test_function_workflow( expected_register: HTTPStatus, expected_get: HTTPStatus, expected_list: HTTPStatus, + expected_update: HTTPStatus, expected_delete: HTTPStatus, expected_get2: HTTPStatus, add_user_function_api_access_rights: AsyncIterator[None], request: pytest.FixtureRequest, ) -> None: + # Register a new function url = client.app.router["register_function"].url_for() response = await client.post(url, json=mock_function) data, error = await assert_status(response, expected_status_code=expected_register) @@ -96,8 +100,9 @@ async def test_function_workflow( assert returned_function.uid is not None returned_function_uid = returned_function.uid + # Get the registered function url = client.app.router["get_function"].url_for( - function_id=str(returned_function_uid) + function_id=f"{returned_function_uid}" ) response = await client.get(url) data, error = await assert_status(response, expected_get) @@ -105,6 +110,7 @@ async def test_function_workflow( retrieved_function = RegisteredProjectFunctionGet.model_validate(data) assert retrieved_function.uid == returned_function.uid + # List existing functions url = client.app.router["list_functions"].url_for() response = await client.get(url) data, error = await assert_status(response, expected_list) @@ -115,14 +121,31 @@ async def test_function_workflow( assert len(retrieved_functions) == 1 assert retrieved_functions[0].uid == returned_function_uid + # Update existing function + new_title = "Test Function (edited)" + new_description = "A test function (edited)" + url = client.app.router["update_function"].url_for( + function_id=f"{returned_function_uid}" + ) + response = await client.patch( + url, json={"title": new_title, "description": new_description} + ) + data, error = await assert_status(response, expected_update) + if not error: + updated_function = RegisteredProjectFunctionGet.model_validate(data) + assert updated_function.title == new_title + assert updated_function.description == new_description + + # Delete existing function url = client.app.router["delete_function"].url_for( - function_id=str(returned_function_uid) + function_id=f"{returned_function_uid}" ) response = await client.delete(url) data, error = await assert_status(response, expected_delete) + # Check if the function was effectively deleted url = client.app.router["get_function"].url_for( - function_id=str(returned_function_uid) + function_id=f"{returned_function_uid}" ) response = await client.get(url) data, error = await assert_status(response, expected_get2) From edc8c2d64ca476e4b6c9363384050c78a087cc59 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Mon, 21 Jul 2025 13:27:25 +0200 Subject: [PATCH 30/69] fix: typecheck --- .../functions/_controller/_functions_rest.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index af2865370fb..d504df22df8 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -39,7 +39,7 @@ routes = web.RouteTableDef() -def _create_json_response_from_page(page: Page[ItemT]): +def _create_json_response_from_page(page: Page[ItemT]) -> web.Response: return web.Response( text=page.model_dump_json(**RESPONSE_MODEL_POLICY), content_type=MIMETYPE_APPLICATION_JSON, @@ -110,12 +110,13 @@ async def list_functions(request: web.Request) -> web.Response: assert isinstance(function, RegisteredProjectFunction) project_ids.append(function.project_id) - projects = await _projects_service.batch_get_projects( - request.app, - project_uuids=project_ids, - ) - for project in projects: - projects_map[f"{project.uuid}"] = project + projects_map = { + f"{p.uuid}": p + for p in await _projects_service.batch_get_projects( + request.app, + project_uuids=project_ids, + ) + } for function in functions: if ( @@ -123,8 +124,7 @@ async def list_functions(request: web.Request) -> web.Response: and function.function_class == FunctionClass.PROJECT ): assert isinstance(function, RegisteredProjectFunction) # nosec - project = projects_map.get(f"{function.project_id}") - if project: + if project := projects_map.get(f"{function.project_id}"): chunk.append( TypeAdapter(RegisteredProjectFunctionGet).validate_python( function.model_dump(mode="json") From b2a189872c9f612968775139a8eb9e142a1752d0 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Mon, 21 Jul 2025 13:44:22 +0200 Subject: [PATCH 31/69] fix: add modified_at field --- services/api-server/tests/unit/api_functions/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/api-server/tests/unit/api_functions/conftest.py b/services/api-server/tests/unit/api_functions/conftest.py index f25334c5a28..7eca63325a9 100644 --- a/services/api-server/tests/unit/api_functions/conftest.py +++ b/services/api-server/tests/unit/api_functions/conftest.py @@ -130,6 +130,7 @@ def mock_registered_project_function(mock_function: Function) -> RegisteredFunct **mock_function.dict(), "uid": str(uuid4()), "created_at": datetime.datetime.now(datetime.UTC), + "modified_at": datetime.datetime.now(datetime.UTC), } ) @@ -150,6 +151,7 @@ def mock_registered_solver_function( "default_inputs": None, "uid": str(uuid4()), "created_at": datetime.datetime.now(datetime.UTC), + "modified_at": datetime.datetime.now(datetime.UTC), "solver_key": "simcore/services/comp/ans-model", "solver_version": "1.0.1", } From b57a590a0cb6708ff69c8bd4fa0258a17250c4d4 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Mon, 21 Jul 2025 14:01:56 +0200 Subject: [PATCH 32/69] fix: deprecated pydantic --- .../tests/unit/api_functions/conftest.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/services/api-server/tests/unit/api_functions/conftest.py b/services/api-server/tests/unit/api_functions/conftest.py index 7eca63325a9..f248fcfcf2d 100644 --- a/services/api-server/tests/unit/api_functions/conftest.py +++ b/services/api-server/tests/unit/api_functions/conftest.py @@ -127,8 +127,8 @@ def mock_function( def mock_registered_project_function(mock_function: Function) -> RegisteredFunction: return RegisteredProjectFunction( **{ - **mock_function.dict(), - "uid": str(uuid4()), + **mock_function.model_dump(), + "uid": f"{uuid4()}", "created_at": datetime.datetime.now(datetime.UTC), "modified_at": datetime.datetime.now(datetime.UTC), } @@ -149,7 +149,7 @@ def mock_registered_solver_function( "input_schema": sample_input_schema, "output_schema": sample_output_schema, "default_inputs": None, - "uid": str(uuid4()), + "uid": f"{uuid4()}", "created_at": datetime.datetime.now(datetime.UTC), "modified_at": datetime.datetime.now(datetime.UTC), "solver_key": "simcore/services/comp/ans-model", @@ -168,7 +168,7 @@ def mock_project_function_job( "description": "A test function job", "inputs": {"key": "value"}, "outputs": None, - "project_job_id": str(uuid4()), + "project_job_id": f"{uuid4()}", "function_class": FunctionClass.PROJECT, } return ProjectFunctionJob(**mock_function_job) @@ -180,8 +180,8 @@ def mock_registered_project_function_job( ) -> RegisteredFunctionJob: return RegisteredProjectFunctionJob( **{ - **mock_project_function_job.dict(), - "uid": str(uuid4()), + **mock_project_function_job.model_dump(), + "uid": f"{uuid4()}", "created_at": datetime.datetime.now(datetime.UTC), } ) @@ -208,8 +208,8 @@ def mock_registered_solver_function_job( ) -> RegisteredFunctionJob: return RegisteredSolverFunctionJob( **{ - **mock_solver_function_job.dict(), - "uid": str(uuid4()), + **mock_solver_function_job.model_dump(), + "uid": f"{uuid4()}", "created_at": datetime.datetime.now(datetime.UTC), } ) @@ -224,7 +224,7 @@ def mock_function_job_collection( "description": "A test function job collection", "function_uid": mock_registered_project_function_job.function_uid, "function_class": FunctionClass.PROJECT, - "project_id": str(uuid4()), + "project_id": f"{uuid4()}", "function_job_ids": [ mock_registered_project_function_job.uid for _ in range(5) ], @@ -239,7 +239,7 @@ def mock_registered_function_job_collection( return RegisteredFunctionJobCollection( **{ **mock_function_job_collection.model_dump(), - "uid": str(uuid4()), + "uid": f"{uuid4()}", "created_at": datetime.datetime.now(datetime.UTC), } ) From 57010e5e51ec4f8680aafdc100f2b0663057d365 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Mon, 21 Jul 2025 14:11:32 +0200 Subject: [PATCH 33/69] fix: make openapi-spec --- .../api/v0/openapi.yaml | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 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 5f83c872432..873083cb713 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 @@ -16090,9 +16090,9 @@ components: const: PROJECT title: Functionclass default: PROJECT - title: + name: type: string - title: Title + title: Name default: '' description: type: string @@ -16120,41 +16120,36 @@ components: type: object - type: 'null' title: Defaultinputs - uid: + uuid: type: string format: uuid - title: Uid - createdAt: + title: Uuid + creationDate: type: string format: date-time - title: Createdat - projectId: + title: Creationdate + lastChangeDate: type: string - format: uuid - title: Projectid - uuid: + format: date-time + title: Lastchangedate + templateId: type: string format: uuid - title: Uuid + title: Templateid thumbnail: anyOf: - type: string - type: 'null' title: Thumbnail - templateId: - anyOf: - - type: integer - - type: 'null' - title: Templateid type: object required: - inputSchema - outputSchema - defaultInputs - - uid - - createdAt - - projectId - uuid + - creationDate + - lastChangeDate + - templateId title: RegisteredProjectFunctionGet RegisteredSolverFunctionGet: properties: @@ -16201,6 +16196,10 @@ components: type: string format: date-time title: Createdat + modifiedAt: + type: string + format: date-time + title: Modifiedat solverKey: type: string pattern: ^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$ @@ -16216,6 +16215,7 @@ components: - defaultInputs - uid - createdAt + - modifiedAt - solverKey - solverVersion title: RegisteredSolverFunctionGet From 606f2f77d0841f02441856836acf76760c8aa777 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Mon, 21 Jul 2025 15:04:41 +0200 Subject: [PATCH 34/69] fix: make openapi-spec --- services/api-server/openapi.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/services/api-server/openapi.json b/services/api-server/openapi.json index 40b9594f3e5..588d54a792f 100644 --- a/services/api-server/openapi.json +++ b/services/api-server/openapi.json @@ -10623,6 +10623,11 @@ "format": "date-time", "title": "Created At" }, + "modified_at": { + "type": "string", + "format": "date-time", + "title": "Modified At" + }, "project_id": { "type": "string", "format": "uuid", @@ -10636,6 +10641,7 @@ "default_inputs", "uid", "created_at", + "modified_at", "project_id" ], "title": "RegisteredProjectFunction" @@ -10782,6 +10788,11 @@ "format": "date-time", "title": "Created At" }, + "modified_at": { + "type": "string", + "format": "date-time", + "title": "Modified At" + }, "code_url": { "type": "string", "title": "Code Url" @@ -10794,6 +10805,7 @@ "default_inputs", "uid", "created_at", + "modified_at", "code_url" ], "title": "RegisteredPythonCodeFunction" @@ -10934,6 +10946,11 @@ "format": "date-time", "title": "Created At" }, + "modified_at": { + "type": "string", + "format": "date-time", + "title": "Modified At" + }, "solver_key": { "type": "string", "pattern": "^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$", @@ -10952,6 +10969,7 @@ "default_inputs", "uid", "created_at", + "modified_at", "solver_key", "solver_version" ], From 89571eb5c75fc22a3ed4f929df2a6e6cabea927b Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Mon, 21 Jul 2025 15:48:09 +0200 Subject: [PATCH 35/69] fix: rename --- .../src/models_library/api_schemas_webserver/functions.py | 1 - .../client/source/class/osparc/data/model/Function.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/functions.py b/packages/models-library/src/models_library/api_schemas_webserver/functions.py index aeadde571e7..01c4ee64df3 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/functions.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/functions.py @@ -118,7 +118,6 @@ class RegisteredSolverFunctionGet(RegisteredSolverFunction, OutputSchema): ... class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): uid: Annotated[FunctionID, Field(alias="uuid")] - title: Annotated[str, Field(alias="name")] = "" project_id: Annotated[ProjectID, Field(alias="templateId")] created_at: Annotated[datetime.datetime, Field(alias="creationDate")] modified_at: Annotated[datetime.datetime, Field(alias="lastChangeDate")] diff --git a/services/static-webserver/client/source/class/osparc/data/model/Function.js b/services/static-webserver/client/source/class/osparc/data/model/Function.js index 58f89f7fc99..1dae7c4047b 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Function.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Function.js @@ -31,7 +31,7 @@ qx.Class.define("osparc.data.model.Function", { this.set({ uuid: functionData.uuid, functionType: functionData.functionClass, - name: functionData.name, + name: functionData.title, description: functionData.description, inputSchema: functionData.inputSchema || this.getInputSchema(), outputSchema: functionData.outputSchema || this.getOutputSchema(), From ebed70d54e43ec77566f27132dc1a80587e4c221 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Tue, 22 Jul 2025 09:21:13 +0200 Subject: [PATCH 36/69] fix: use common create_json_response_from_page --- .../folders/_folders_rest.py | 17 ++++------------- .../functions/_controller/_functions_rest.py | 15 +++------------ 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/folders/_folders_rest.py b/services/web/server/src/simcore_service_webserver/folders/_folders_rest.py index 5219a500f1a..b43c75ca571 100644 --- a/services/web/server/src/simcore_service_webserver/folders/_folders_rest.py +++ b/services/web/server/src/simcore_service_webserver/folders/_folders_rest.py @@ -8,7 +8,7 @@ ) from models_library.folders import FolderTuple from models_library.rest_ordering import OrderBy -from models_library.rest_pagination import ItemT, Page +from models_library.rest_pagination import Page from models_library.rest_pagination_utils import paginate_data from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( @@ -16,13 +16,11 @@ parse_request_path_parameters_as, parse_request_query_parameters_as, ) -from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON -from servicelib.rest_constants import RESPONSE_MODEL_POLICY from .._meta import API_VTAG as VTAG from ..login.decorators import login_required from ..security.decorators import permission_required -from ..utils_aiohttp import envelope_json_response +from ..utils_aiohttp import create_json_response_from_page, envelope_json_response from . import _folders_service from ._common.exceptions_handlers import handle_plugin_requests_exceptions from ._common.models import ( @@ -39,13 +37,6 @@ routes = web.RouteTableDef() -def _create_json_response_from_page(page: Page[ItemT]): - return web.Response( - text=page.model_dump_json(**RESPONSE_MODEL_POLICY), - content_type=MIMETYPE_APPLICATION_JSON, - ) - - @routes.post(f"/{VTAG}/folders", name="create_folder") @login_required @permission_required("folder.create") @@ -107,7 +98,7 @@ async def list_folders(request: web.Request): offset=query_params.offset, ) ) - return _create_json_response_from_page(page) + return create_json_response_from_page(page) @routes.get(f"/{VTAG}/folders:search", name="list_folders_full_search") @@ -143,7 +134,7 @@ async def list_folders_full_search(request: web.Request): offset=query_params.offset, ) ) - return _create_json_response_from_page(page) + return create_json_response_from_page(page) @routes.get(f"/{VTAG}/folders/{{folder_id}}", name="get_folder") diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index c2d56756eab..4486aa04bec 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -9,7 +9,7 @@ ) from models_library.api_schemas_webserver.users import MyFunctionPermissionsGet from models_library.functions import FunctionClass, RegisteredProjectFunction -from models_library.rest_pagination import ItemT, Page +from models_library.rest_pagination import Page from models_library.rest_pagination_utils import paginate_data from pydantic import TypeAdapter from servicelib.aiohttp import status @@ -18,8 +18,6 @@ parse_request_path_parameters_as, parse_request_query_parameters_as, ) -from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON -from servicelib.rest_constants import RESPONSE_MODEL_POLICY from ..._meta import API_VTAG as VTAG from ...login.decorators import login_required @@ -27,7 +25,7 @@ from ...projects import _projects_service from ...projects.models import ProjectDBGet from ...security.decorators import permission_required -from ...utils_aiohttp import envelope_json_response +from ...utils_aiohttp import create_json_response_from_page, envelope_json_response from .. import _functions_service from ._functions_rest_exceptions import handle_rest_requests_exceptions from ._functions_rest_schemas import ( @@ -39,13 +37,6 @@ routes = web.RouteTableDef() -def _create_json_response_from_page(page: Page[ItemT]) -> web.Response: - return web.Response( - text=page.model_dump_json(**RESPONSE_MODEL_POLICY), - content_type=MIMETYPE_APPLICATION_JSON, - ) - - @routes.post(f"/{VTAG}/functions", name="register_function") @login_required @handle_rest_requests_exceptions @@ -150,7 +141,7 @@ async def list_functions(request: web.Request) -> web.Response: offset=query_params.offset, ) ) - return _create_json_response_from_page(page) + return create_json_response_from_page(page) @routes.get( From d03ac5936cab63be26f39b4632da931ddb44616b Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Tue, 22 Jul 2025 09:40:45 +0200 Subject: [PATCH 37/69] fix: update field --- .../server/src/simcore_service_webserver/api/v0/openapi.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 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 05b6f6c92ec..e65828c3803 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 @@ -16035,9 +16035,9 @@ components: const: PROJECT title: Functionclass default: PROJECT - name: + title: type: string - title: Name + title: Title default: '' description: type: string From 86e692efbef0a5353f86f6531234b1cb45883b93 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Tue, 22 Jul 2025 09:53:09 +0200 Subject: [PATCH 38/69] remove dummy response --- .../source/class/osparc/store/Functions.js | 91 ------------------- 1 file changed, 91 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/store/Functions.js b/services/static-webserver/client/source/class/osparc/store/Functions.js index 7e951e6c327..7b489ac7537 100644 --- a/services/static-webserver/client/source/class/osparc/store/Functions.js +++ b/services/static-webserver/client/source/class/osparc/store/Functions.js @@ -92,14 +92,6 @@ qx.Class.define("osparc.store.Functions", { }, fetchFunctionsPaginated: function(params, options) { - const isBackendReady = true; - if (!isBackendReady) { - return new Promise(resolve => { - const response = this.__dummyResponse(); - response["params"] = params; - resolve(response); - }); - } return osparc.data.Resources.fetch("functions", "getPage", params, options) .then(response => { const functions = response["data"]; @@ -110,13 +102,6 @@ qx.Class.define("osparc.store.Functions", { }, fetchFunction: function(functionId) { - const isBackendReady = true; - if (!isBackendReady) { - return new Promise(resolve => { - const response = this.__dummyResponse(); - resolve(response["data"][0]); - }); - } const params = { url: { "functionId": functionId @@ -136,81 +121,5 @@ qx.Class.define("osparc.store.Functions", { this.__functionsPromiseCached = null; } }, - - __dummyResponse: function() { - return { - "_meta": { - "limit": 10, - "total": 1, - "offset": 0, - "count": 1 - }, - "data": [{ - "uuid": "0fab79c3-14b8-4625-a455-6dcbf74eb4f2", - "functionClass": "PROJECT", - "name": "Potential Function II", - "description": "Function description", - "inputSchema": { - "schema_class": "application/schema+json", - "schema_content": { - "type": "object", - "required": [ - "X" - ], - "properties": { - "X": { - "type": "number" - } - } - } - }, - "outputSchema": { - "schema_class": "application/schema+json", - "schema_content": { - "type": "object", - "required": [ - "Out 1", - "Out_2" - ], - "properties": { - "Out 1": { - "type": "number" - }, - "Out_2": { - "type": "number" - } - } - } - }, - "defaultInputs": { - "X": 2, - "Y": 1 - }, - "creationDate": "2025-05-16T12:22:31.063Z", - "lastChangeDate": "2025-05-16T12:22:33.804Z", - "accessRights": { - "3": { - "read": true, - "write": true, - "delete": true - }, - "5": { - "read": true, - "write": false, - "delete": false - } - }, - "thumbnail": "https://img.freepik.com/premium-vector/image-icon-design-vector-template_1309674-940.jpg", - "workbench": {"50a50309-1dfc-5ad5-b2d9-c11697641f0b": {"key": "simcore/services/comp/itis/sleeper", "version": "2.1.6", "label": "sleeper", "inputs": {"input_2": 2, "input_3": false, "input_4": 0, "input_5": 0}, "inputsRequired": [], "inputNodes": ["2e348481-5042-5148-9196-590574747297", "69873032-770a-536b-adb6-0e6ea01720a4"]}, "2e348481-5042-5148-9196-590574747297": {"key": "simcore/services/frontend/parameter/number", "version": "1.0.0", "label": "X", "inputs": {}, "inputsRequired": [], "inputNodes": [], "outputs": {"out_1": 1}, "runHash": null}, "70e1de1a-a8b0-59e3-b19e-ea20f78765ce": {"key": "simcore/services/frontend/iterator-consumer/probe/number", "version": "1.0.0", "label": "Out 1", "inputs": {"in_1": 0}, "inputsRequired": [], "inputNodes": ["50a50309-1dfc-5ad5-b2d9-c11697641f0b"]}, "69873032-770a-536b-adb6-0e6ea01720a4": {"key": "simcore/services/frontend/parameter/number", "version": "1.0.0", "label": "Y", "inputs": {}, "inputsRequired": [], "inputNodes": [], "outputs": {"out_1": 1}, "runHash": null}, "24f856c3-408c-5ab4-ad01-e99630a355fe": {"key": "simcore/services/frontend/iterator-consumer/probe/number", "version": "1.0.0", "label": "Out_2", "inputs": {"in_1": 0}, "inputsRequired": [], "inputNodes": ["50a50309-1dfc-5ad5-b2d9-c11697641f0b"]}}, - "ui": { - "workbench": {"24f856c3-408c-5ab4-ad01-e99630a355fe": {"position": {"x": 540, "y": 240}}, "2e348481-5042-5148-9196-590574747297": {"position": {"x": 120, "y": 140}}, "50a50309-1dfc-5ad5-b2d9-c11697641f0b": {"position": {"x": 300, "y": 180}}, "69873032-770a-536b-adb6-0e6ea01720a4": {"position": {"x": 120, "y": 240}}, "70e1de1a-a8b0-59e3-b19e-ea20f78765ce": {"position": {"x": 540, "y": 140}}}, - "mode": "pipeline", - }, - }], - "_links": { - "next": null, - }, - }; - }, } }); From a8b23877eb343025d6b4f002a206060c4d09afa3 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Tue, 22 Jul 2025 09:53:58 +0200 Subject: [PATCH 39/69] readFunctions from backend --- .../client/source/class/osparc/data/Permissions.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/data/Permissions.js b/services/static-webserver/client/source/class/osparc/data/Permissions.js index 477b08ffd66..8d0a957abc9 100644 --- a/services/static-webserver/client/source/class/osparc/data/Permissions.js +++ b/services/static-webserver/client/source/class/osparc/data/Permissions.js @@ -310,11 +310,6 @@ qx.Class.define("osparc.data.Permissions", { return false; } - // This needs to be provided by the backend - if (action === "readFunctions") { - return osparc.utils.Utils.isDevelopmentPlatform(); - } - if ( this.__functionPermissions && action in this.__functionPermissions From 945f2318832605fe3d8a62899926da6582e6f48e Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Tue, 22 Jul 2025 10:32:32 +0200 Subject: [PATCH 40/69] feat: add accessRights --- .../src/models_library/api_schemas_webserver/functions.py | 2 ++ .../functions/_controller/_functions_rest.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/functions.py b/packages/models-library/src/models_library/api_schemas_webserver/functions.py index 01c4ee64df3..86dd30545ee 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/functions.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/functions.py @@ -5,6 +5,7 @@ from ..functions import ( Function, + FunctionAccessRights, FunctionBase, FunctionClass, FunctionClassSpecificData, @@ -121,6 +122,7 @@ class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): project_id: Annotated[ProjectID, Field(alias="templateId")] created_at: Annotated[datetime.datetime, Field(alias="creationDate")] modified_at: Annotated[datetime.datetime, Field(alias="lastChangeDate")] + access_rights: FunctionAccessRights thumbnail: str | None = None diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 4486aa04bec..e1c10f04ae3 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -119,6 +119,12 @@ async def list_functions(request: web.Request) -> web.Response: TypeAdapter(RegisteredProjectFunctionGet).validate_python( function.model_dump(mode="json") | { + "accessRights": await _functions_service.get_function_user_permissions( + request.app, + user_id=req_ctx.user_id, + function_id=function.uid, + product_name=req_ctx.product_name, + ), "thumbnail": ( f"{project.thumbnail}" if project.thumbnail else None ), From c6d9948035199173ea7a847344fdb3fef579f7e6 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Tue, 22 Jul 2025 10:37:23 +0200 Subject: [PATCH 41/69] users pending dates --- .../source/class/osparc/po/UsersPending.js | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/po/UsersPending.js b/services/static-webserver/client/source/class/osparc/po/UsersPending.js index b2e4e1e2973..0da1fa091a7 100644 --- a/services/static-webserver/client/source/class/osparc/po/UsersPending.js +++ b/services/static-webserver/client/source/class/osparc/po/UsersPending.js @@ -164,14 +164,26 @@ qx.Class.define("osparc.po.UsersPending", { row, column: 0, }); + pendingUsersLayout.add(new qx.ui.basic.Label(pendingUser.email), { row, column: 1, }); - pendingUsersLayout.add(new qx.ui.basic.Label(pendingUser.accountRequestReviewedAt ? osparc.utils.Utils.formatDateAndTime(new Date(pendingUser.accountRequestReviewedAt)) : "-"), { + + let date = null; + switch (pendingUser.accountRequestStatus) { + case "PENDING": + date = pendingUser.preRegistrationRequestedAt ? osparc.utils.Utils.formatDateAndTime(new Date(pendingUser.preRegistrationRequestedAt)) : "-"; + break; + default: + date = pendingUser.accountRequestReviewedAt ? osparc.utils.Utils.formatDateAndTime(new Date(pendingUser.accountRequestReviewedAt)) : "-"; + break; + } + pendingUsersLayout.add(date, { row, column: 2, }); + const statusChip = new osparc.ui.basic.Chip().set({ label: pendingUser.accountRequestStatus.toLowerCase(), }); @@ -182,17 +194,18 @@ qx.Class.define("osparc.po.UsersPending", { row, column: 3, }); + const infoButton = this.self().createInfoButton(pendingUser); pendingUsersLayout.add(infoButton, { row, column: 4, }); + const buttonsLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)); pendingUsersLayout.add(buttonsLayout, { row, column: 5, }); - switch (pendingUser.accountRequestStatus) { case "PENDING": { statusChip.setStatusColor(osparc.ui.basic.Chip.STATUS.WARNING); @@ -233,8 +246,18 @@ qx.Class.define("osparc.po.UsersPending", { const pendingUsers = resps[0]; const reviewedUsers = resps[1]; const sortByDate = (a, b) => { - const dateA = a.accountRequestReviewedAt ? new Date(a.accountRequestReviewedAt) : new Date(0); - const dateB = b.accountRequestReviewedAt ? new Date(b.accountRequestReviewedAt) : new Date(0); + let dateA = new Date(0); // default to epoch if no date is available + if (a.accountRequestStatus === "PENDING" && a.preRegistrationRequestedAt) { + dateA = new Date(a.preRegistrationRequestedAt) + } else if (a.accountRequestReviewedAt) { + dateA = new Date(a.accountRequestReviewedAt); + } + let dateB = new Date(0); // default to epoch if no date is available + if (b.accountRequestStatus === "PENDING" && b.preRegistrationRequestedAt) { + dateB = new Date(b.preRegistrationRequestedAt) + } else if (b.accountRequestReviewedAt) { + dateB = new Date(b.accountRequestReviewedAt); + } return dateB - dateA; // sort by most recent first }; pendingUsers.sort(sortByDate); From 048591339576cb0d7ca48c70023796839212a4c7 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Tue, 22 Jul 2025 10:37:55 +0200 Subject: [PATCH 42/69] support also title --- .../client/source/class/osparc/dashboard/CardBase.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js b/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js index 6f875d19bb8..e3d26e7c297 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js @@ -553,7 +553,7 @@ qx.Class.define("osparc.dashboard.CardBase", { this.set({ resourceType: resourceData.resourceType, uuid, - title: resourceData.name, + title: resourceData.name || resourceData.title, // title is used by functions description: resourceData.description, owner, accessRights: resourceData.accessRights ? resourceData.accessRights : {}, From 5e3f9f65b3a6a848e5023cc4a7a04e78046218e4 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Tue, 22 Jul 2025 10:43:27 +0200 Subject: [PATCH 43/69] fix: rename field --- .../functions/_controller/_functions_rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index e1c10f04ae3..9bbce3cc48d 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -119,7 +119,7 @@ async def list_functions(request: web.Request) -> web.Response: TypeAdapter(RegisteredProjectFunctionGet).validate_python( function.model_dump(mode="json") | { - "accessRights": await _functions_service.get_function_user_permissions( + "access_rights": await _functions_service.get_function_user_permissions( request.app, user_id=req_ctx.user_id, function_id=function.uid, From 740c5516cdd3a8df6263eb46b2810250834c6ac8 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Tue, 22 Jul 2025 10:46:47 +0200 Subject: [PATCH 44/69] fix: make openapi-spec --- .../api/v0/openapi.yaml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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 e65828c3803..7c377a655cf 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 @@ -12324,6 +12324,23 @@ components: required: - name title: FolderReplaceBodyParams + FunctionAccessRights: + properties: + read: + type: boolean + title: Read + default: false + write: + type: boolean + title: Write + default: false + execute: + type: boolean + title: Execute + default: false + additionalProperties: false + type: object + title: FunctionAccessRights GetProjectInactivityResponse: properties: is_inactive: @@ -16081,6 +16098,8 @@ components: type: string format: uuid title: Templateid + accessRights: + $ref: '#/components/schemas/FunctionAccessRights' thumbnail: anyOf: - type: string @@ -16095,6 +16114,7 @@ components: - creationDate - lastChangeDate - templateId + - accessRights title: RegisteredProjectFunctionGet RegisteredSolverFunctionGet: properties: From b900510a45b7adf58a1e288bd15f12811016388f Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Tue, 22 Jul 2025 11:12:17 +0200 Subject: [PATCH 45/69] fix: order --- .../functions/_controller/_functions_rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 9bbce3cc48d..0030562a0eb 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -122,8 +122,8 @@ async def list_functions(request: web.Request) -> web.Response: "access_rights": await _functions_service.get_function_user_permissions( request.app, user_id=req_ctx.user_id, - function_id=function.uid, product_name=req_ctx.product_name, + function_id=function.uid, ), "thumbnail": ( f"{project.thumbnail}" if project.thumbnail else None From d440ad0abf0b9e93a30383cc1c954c809de55ea6 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Tue, 22 Jul 2025 11:21:39 +0200 Subject: [PATCH 46/69] fix: access_rights not required --- .../src/models_library/api_schemas_webserver/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/functions.py b/packages/models-library/src/models_library/api_schemas_webserver/functions.py index 86dd30545ee..43e41961076 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/functions.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/functions.py @@ -122,7 +122,7 @@ class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): project_id: Annotated[ProjectID, Field(alias="templateId")] created_at: Annotated[datetime.datetime, Field(alias="creationDate")] modified_at: Annotated[datetime.datetime, Field(alias="lastChangeDate")] - access_rights: FunctionAccessRights + access_rights: FunctionAccessRights | None = None thumbnail: str | None = None From 92b7c1afb262a24935883b32a5815135dcae9a57 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Tue, 22 Jul 2025 11:44:02 +0200 Subject: [PATCH 47/69] minor --- .../client/source/class/osparc/dashboard/ResourceDetails.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js index 54a0f3f20fd..9f42d0fd78c 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js @@ -121,7 +121,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { popUpInWindow: function(resourceData) { const resourceDetails = new osparc.dashboard.ResourceDetails(resourceData); - const title = resourceData.name; + const title = resourceData.name || resourceData.title; // title is used by functions const window = osparc.ui.window.Window.popUpInWindow(resourceDetails, title, this.WIDTH, this.HEIGHT).set({ layout: new qx.ui.layout.Grow(), ...osparc.ui.window.TabbedWindow.DEFAULT_PROPS, From dcb1dfcebe8145c4935c85f60c07decfa9641011 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Tue, 22 Jul 2025 11:50:20 +0200 Subject: [PATCH 48/69] add pagination args --- .../client/source/class/osparc/data/Resources.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/data/Resources.js b/services/static-webserver/client/source/class/osparc/data/Resources.js index e9f36a4cea4..fda2bff08c9 100644 --- a/services/static-webserver/client/source/class/osparc/data/Resources.js +++ b/services/static-webserver/client/source/class/osparc/data/Resources.js @@ -628,18 +628,18 @@ qx.Class.define("osparc.data.Resources", { "functions": { useCache: false, endpoints: { - create: { - method: "POST", - url: statics.API + "/functions" - }, getOne: { method: "GET", url: statics.API + "/functions/{functionId}?include_extras=true" }, getPage: { method: "GET", - url: statics.API + "/functions?include_extras=true" - } + url: statics.API + "/functions?include_extras=true&offset={offset}&limit={limit}" + }, + create: { + method: "POST", + url: statics.API + "/functions" + }, } }, /* From 66c3a89f89b94ab531f94456b9c474b70dcc1ebf Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Tue, 22 Jul 2025 13:18:07 +0200 Subject: [PATCH 49/69] template as property --- .../class/osparc/dashboard/ResourceDetails.js | 19 +++++++++++-------- .../class/osparc/data/model/Function.js | 16 ++++------------ 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js index 9f42d0fd78c..36ec9ee6d70 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js @@ -75,14 +75,17 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { }); break; case "function": { - // use function's underlying template info to fetch services metadata - // osparc.store.Templates.fetchTemplate(resourceData["uuid"]); - osparc.store.Services.getStudyServicesMetadata(latestResourceData) - .finally(() => { - this.__resourceModel = new osparc.data.model.Function(latestResourceData); - this.__resourceModel["resourceType"] = resourceData["resourceType"]; - this.__resourceData["services"] = resourceData["services"]; - this.__addPages(); + osparc.store.Templates.fetchTemplate(resourceData["templateId"]) + .then(templateData => { + osparc.store.Services.getStudyServicesMetadata(templateData) + .finally(() => { + this.__resourceModel = new osparc.data.model.Function(latestResourceData); + this.__resourceModel["resourceType"] = resourceData["resourceType"]; + const template = new osparc.data.model.Study(templateData); + this.__resourceModel.setTemplate(template); + this.__resourceData["services"] = resourceData["services"]; + this.__addPages(); + }); }); break; } diff --git a/services/static-webserver/client/source/class/osparc/data/model/Function.js b/services/static-webserver/client/source/class/osparc/data/model/Function.js index 1dae7c4047b..a33e726840c 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Function.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Function.js @@ -40,8 +40,6 @@ qx.Class.define("osparc.data.model.Function", { creationDate: functionData.creationDate ? new Date(functionData.creationDate) : this.getCreationDate(), lastChangeDate: functionData.lastChangeDate ? new Date(functionData.lastChangeDate) : this.getLastChangeDate(), thumbnail: functionData.thumbnail || this.getThumbnail(), - workbenchData: functionData.workbench || this.getWorkbenchData(), - functionUIData: functionData.ui || this.getFunctionUIData(), }); }, @@ -123,16 +121,10 @@ qx.Class.define("osparc.data.model.Function", { init: null }, - workbenchData: { - check: "Object", - nullable: false, - init: {}, - }, - - functionUIData: { - check: "Object", - nullable: false, - init: {}, + template: { + check: "osparc.data.model.Study", + nullable: true, + init: null, }, }, From 4d3398da0125d9c91b64753752a89b60370cd3cc Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Tue, 22 Jul 2025 13:23:08 +0200 Subject: [PATCH 50/69] setTemplate in constructor --- .../source/class/osparc/dashboard/ResourceDetails.js | 4 +--- .../client/source/class/osparc/data/model/Function.js | 8 +++++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js index 36ec9ee6d70..24fabcac3d0 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js @@ -79,10 +79,8 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { .then(templateData => { osparc.store.Services.getStudyServicesMetadata(templateData) .finally(() => { - this.__resourceModel = new osparc.data.model.Function(latestResourceData); + this.__resourceModel = new osparc.data.model.Function(latestResourceData, templateData); this.__resourceModel["resourceType"] = resourceData["resourceType"]; - const template = new osparc.data.model.Study(templateData); - this.__resourceModel.setTemplate(template); this.__resourceData["services"] = resourceData["services"]; this.__addPages(); }); diff --git a/services/static-webserver/client/source/class/osparc/data/model/Function.js b/services/static-webserver/client/source/class/osparc/data/model/Function.js index a33e726840c..d5d5b80a5d7 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Function.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Function.js @@ -24,8 +24,9 @@ qx.Class.define("osparc.data.model.Function", { /** * @param functionData {Object} Object containing the serialized Function Data + * @param templateData {Object} Object containing the underlying serialized Template Data */ - construct: function(functionData) { + construct: function(functionData, templateData = null) { this.base(arguments); this.set({ @@ -41,6 +42,11 @@ qx.Class.define("osparc.data.model.Function", { lastChangeDate: functionData.lastChangeDate ? new Date(functionData.lastChangeDate) : this.getLastChangeDate(), thumbnail: functionData.thumbnail || this.getThumbnail(), }); + + if (templateData) { + const template = new osparc.data.model.Study(templateData); + this.setTemplate(template); + } }, properties: { From ab4acb51533f8c0a6dba3a4b6c4305a18966d0f2 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Tue, 22 Jul 2025 13:29:05 +0200 Subject: [PATCH 51/69] add Preview Page --- .../client/source/class/osparc/dashboard/ResourceDetails.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js index 24fabcac3d0..a232aad7cb2 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js @@ -399,7 +399,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { } else if (this.__resourceData["resourceType"] === "function") { this.__addInfoPage(); // to build the preview page we need the underlying template data - // this.__addPreviewPage(); + this.__addPreviewPage(); this.fireEvent("pagesAdded"); return; } @@ -576,12 +576,12 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { const page = new osparc.dashboard.resources.pages.BasePage(title, iconSrc, id); this.__addToolbarButtons(page); - const studyData = this.__resourceData; + const studyData = osparc.utils.Resources.isFunction(this.__resourceData) ? this.__resourceModel.getTemplate().serialize() : this.__resourceData; const enabled = osparc.study.Utils.canShowPreview(studyData); page.setEnabled(enabled); const lazyLoadContent = () => { - const resourceModel = this.__resourceModel; + const resourceModel = osparc.utils.Resources.isFunction(this.__resourceData) ? this.__resourceModel.getTemplate() : this.__resourceData; const preview = new osparc.study.StudyPreview(resourceModel); page.addToContent(preview); this.__widgets.push(preview); From d5ec202091fb6bd2cb9460fd9ad66ed4a0c35eb8 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Tue, 22 Jul 2025 13:39:44 +0200 Subject: [PATCH 52/69] support all three functionClasses --- .../class/osparc/dashboard/ResourceDetails.js | 39 ++++++++++++------- .../class/osparc/data/model/Function.js | 8 ++-- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js index a232aad7cb2..31a9c77eadb 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js @@ -66,6 +66,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { const studyStore = osparc.store.Study.getInstance(); this.__resourceData["debt"] = studyStore.getStudyDebt(this.__resourceData["uuid"]); } + // prefetch project's services metadata osparc.store.Services.getStudyServicesMetadata(latestResourceData) .finally(() => { this.__resourceModel = new osparc.data.model.Study(latestResourceData); @@ -75,16 +76,25 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { }); break; case "function": { - osparc.store.Templates.fetchTemplate(resourceData["templateId"]) - .then(templateData => { - osparc.store.Services.getStudyServicesMetadata(templateData) - .finally(() => { - this.__resourceModel = new osparc.data.model.Function(latestResourceData, templateData); - this.__resourceModel["resourceType"] = resourceData["resourceType"]; - this.__resourceData["services"] = resourceData["services"]; - this.__addPages(); - }); - }); + addPages = () => { + this.__resourceModel = new osparc.data.model.Function(latestResourceData, templateData); + this.__resourceModel["resourceType"] = resourceData["resourceType"]; + this.__addPages(); + } + if (resourceData["functionClass"] === "PROJECT") { + // this is only required for functions that have a template linked + osparc.store.Templates.fetchTemplate(resourceData["templateId"]) + .then(templateData => { + // prefetch function's underlying template's services metadata + osparc.store.Services.getStudyServicesMetadata(templateData) + .finally(() => { + this.__resourceData["services"] = resourceData["services"]; + addPages(); + }); + }); + } else { + addPages(); + } break; } case "service": { @@ -174,7 +184,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { __addToolbarButtons: function(page) { const resourceData = this.__resourceData; - if (this.__resourceData["resourceType"] === "function") { + if (osparc.utils.Resources.isFunction(this.__resourceData)) { return; // no toolbar buttons for functions } @@ -396,10 +406,11 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { this.__addPreviewPage(); this.fireEvent("pagesAdded"); return; - } else if (this.__resourceData["resourceType"] === "function") { + } else if (osparc.utils.Resources.isFunction(this.__resourceData)) { this.__addInfoPage(); - // to build the preview page we need the underlying template data - this.__addPreviewPage(); + if (this.__resourceModel.getFunctionClass() === "PROJECT") { + this.__addPreviewPage(); + } this.fireEvent("pagesAdded"); return; } diff --git a/services/static-webserver/client/source/class/osparc/data/model/Function.js b/services/static-webserver/client/source/class/osparc/data/model/Function.js index d5d5b80a5d7..f27a25d5b37 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Function.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Function.js @@ -31,7 +31,7 @@ qx.Class.define("osparc.data.model.Function", { this.set({ uuid: functionData.uuid, - functionType: functionData.functionClass, + functionClass: functionData.functionClass, name: functionData.title, description: functionData.description, inputSchema: functionData.inputSchema || this.getInputSchema(), @@ -57,10 +57,10 @@ qx.Class.define("osparc.data.model.Function", { init: "" }, - functionType: { - check: ["PROJECT"], + functionClass: { + check: ["PROJECT", "SOLVER", "PYTHON_CODE"], nullable: false, - event: "changeFunctionType", + event: "changeFunctionClass", init: null }, From 0ea2c683107d012c5dfed2baaebbe8bbf3a1beda Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Tue, 22 Jul 2025 13:42:34 +0200 Subject: [PATCH 53/69] minor --- .../source/class/osparc/dashboard/ResourceDetails.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js index 31a9c77eadb..517cd864fa9 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js @@ -76,8 +76,8 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { }); break; case "function": { - addPages = () => { - this.__resourceModel = new osparc.data.model.Function(latestResourceData, templateData); + addPages = (functionData, templateData = null) => { + this.__resourceModel = new osparc.data.model.Function(functionData, templateData); this.__resourceModel["resourceType"] = resourceData["resourceType"]; this.__addPages(); } @@ -89,11 +89,11 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { osparc.store.Services.getStudyServicesMetadata(templateData) .finally(() => { this.__resourceData["services"] = resourceData["services"]; - addPages(); + addPages(latestResourceData, templateData); }); }); } else { - addPages(); + addPages(latestResourceData); } break; } From 8a1dfe1ec4c9c5755bb7a046c6e1f4af45a2ab46 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Tue, 22 Jul 2025 14:11:53 +0200 Subject: [PATCH 54/69] accessRights -> myAccessRights --- .../class/osparc/data/model/Function.js | 20 +++---------------- .../source/class/osparc/info/FunctionLarge.js | 6 +++--- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/data/model/Function.js b/services/static-webserver/client/source/class/osparc/data/model/Function.js index f27a25d5b37..90599e5e153 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Function.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Function.js @@ -37,7 +37,7 @@ qx.Class.define("osparc.data.model.Function", { inputSchema: functionData.inputSchema || this.getInputSchema(), outputSchema: functionData.outputSchema || this.getOutputSchema(), defaultInputs: functionData.defaultInputs || this.getDefaultInputs(), - accessRights: functionData.accessRights || this.getAccessRights(), + myAccessRights: functionData.accessRights || this.getMyAccessRights(), creationDate: functionData.creationDate ? new Date(functionData.creationDate) : this.getCreationDate(), lastChangeDate: functionData.lastChangeDate ? new Date(functionData.lastChangeDate) : this.getLastChangeDate(), thumbnail: functionData.thumbnail || this.getThumbnail(), @@ -99,10 +99,10 @@ qx.Class.define("osparc.data.model.Function", { init: {} }, - accessRights: { + myAccessRights: { check: "Object", nullable: false, - event: "changeAccessRights", + event: "changeMyAccessRights", init: {} }, @@ -134,20 +134,6 @@ qx.Class.define("osparc.data.model.Function", { }, }, - statics: { - canIWrite: function(accessRights) { - const groupsStore = osparc.store.Groups.getInstance(); - const gIds = groupsStore.getOrganizationIds(); - gIds.push(groupsStore.getMyGroupId()); - let canWrite = false; - for (let i=0; i Date: Tue, 22 Jul 2025 14:14:55 +0200 Subject: [PATCH 55/69] fix: add accessRights when getting single function --- .../functions/_controller/_functions_rest.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 0030562a0eb..d0f661b86f6 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -189,6 +189,12 @@ async def get_function(request: web.Request) -> web.Response: TypeAdapter(RegisteredProjectFunctionGet).validate_python( registered_function.model_dump(mode="json") | { + "access_rights": await _functions_service.get_function_user_permissions( + request.app, + user_id=req_ctx.user_id, + product_name=req_ctx.product_name, + function_id=function_id, + ), "thumbnail": project_dict.get("thumbnail", None), } ) From fc80afa60f3cfbcfd1682c0e7edf0b01a7493292 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Tue, 22 Jul 2025 14:17:52 +0200 Subject: [PATCH 56/69] refactor --- .../client/source/class/osparc/dashboard/CardBase.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js b/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js index e3d26e7c297..e8f378aaea3 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js @@ -524,6 +524,7 @@ qx.Class.define("osparc.dashboard.CardBase", { __applyResourceData: function(resourceData) { let uuid = null; + let title = ""; let owner = null; let workbench = null; let defaultHits = null; @@ -534,16 +535,19 @@ qx.Class.define("osparc.dashboard.CardBase", { case "tutorial": case "hypertool": uuid = resourceData.uuid ? resourceData.uuid : null; + title = resourceData.name, owner = resourceData.prjOwner ? resourceData.prjOwner : ""; workbench = resourceData.workbench ? resourceData.workbench : {}; break; case "function": uuid = resourceData.uuid ? resourceData.uuid : null; + title = resourceData.title, owner = ""; workbench = resourceData.workbench ? resourceData.workbench : {}; break; case "service": uuid = resourceData.key ? resourceData.key : null; + title = resourceData.name, owner = resourceData.owner ? resourceData.owner : resourceData.contact; icon = resourceData["icon"] || osparc.dashboard.CardBase.PRODUCT_ICON; defaultHits = 0; @@ -553,7 +557,7 @@ qx.Class.define("osparc.dashboard.CardBase", { this.set({ resourceType: resourceData.resourceType, uuid, - title: resourceData.name || resourceData.title, // title is used by functions + title, description: resourceData.description, owner, accessRights: resourceData.accessRights ? resourceData.accessRights : {}, From df12a5fd5252e02fb1229158abb4eddb3218c273 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Tue, 22 Jul 2025 14:57:54 +0200 Subject: [PATCH 57/69] show "owner" --- .../source/class/osparc/dashboard/GridButtonItem.js | 11 ++++++++--- .../source/class/osparc/dashboard/ListButtonItem.js | 9 +++++++-- .../client/source/class/osparc/info/FunctionLarge.js | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/dashboard/GridButtonItem.js b/services/static-webserver/client/source/class/osparc/dashboard/GridButtonItem.js index cd9d02ea0bb..e9fbc461413 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/GridButtonItem.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/GridButtonItem.js @@ -249,9 +249,14 @@ qx.Class.define("osparc.dashboard.GridButtonItem", { // overridden _applyOwner: function(value, old) { const label = this.getChildControl("subtitle-text"); - const user = this.__createOwner(value); - label.setValue(user); - label.setVisibility(value ? "visible" : "excluded"); + if (osparc.utils.Resources.isFunction(this.getResourceData())) { + const canIWrite = Boolean(this.getResourceData()["accessRights"]["write"]); + label.setValue(canIWrite ? "My Function" : "Read Only"); + } else { + const user = this.__createOwner(value); + label.setValue(user); + label.setVisibility(value ? "visible" : "excluded"); + } }, _applyAccessRights: function(value) { diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ListButtonItem.js b/services/static-webserver/client/source/class/osparc/dashboard/ListButtonItem.js index 8139bedf0a2..f8cd7c13dfa 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ListButtonItem.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ListButtonItem.js @@ -243,8 +243,13 @@ qx.Class.define("osparc.dashboard.ListButtonItem", { _applyOwner: function(value, old) { const label = this.getChildControl("owner"); - const user = this.__createOwner(value); - label.setValue(user); + if (osparc.utils.Resources.isFunction(this.getResourceData())) { + const canIWrite = Boolean(this.getResourceData()["accessRights"]["write"]); + label.setValue(canIWrite ? "My Function" : "Read Only"); + } else { + const user = this.__createOwner(value); + label.setValue(user); + } this.__makeItemResponsive(label); }, diff --git a/services/static-webserver/client/source/class/osparc/info/FunctionLarge.js b/services/static-webserver/client/source/class/osparc/info/FunctionLarge.js index 5144c5fd891..23307a35aab 100644 --- a/services/static-webserver/client/source/class/osparc/info/FunctionLarge.js +++ b/services/static-webserver/client/source/class/osparc/info/FunctionLarge.js @@ -120,7 +120,7 @@ qx.Class.define("osparc.info.FunctionLarge", { }, "ACCESS_RIGHTS": { label: this.tr("Permissions"), - view: new qx.ui.basic.Label(canIWrite ? "Owner" : "Read Only"), + view: new qx.ui.basic.Label(canIWrite ? "My Function" : "Read Only"), action: null }, "CREATED": { From 3839c4f703c988f06c696d9cb47db18355aed7a7 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Tue, 22 Jul 2025 15:24:12 +0200 Subject: [PATCH 58/69] patchFunction --- .../source/class/osparc/data/Resources.js | 4 + .../class/osparc/data/model/Function.js | 26 +++-- .../source/class/osparc/info/FunctionLarge.js | 21 ++-- .../source/class/osparc/info/FunctionUtils.js | 103 ++++++++++++++++++ .../source/class/osparc/store/Functions.js | 16 ++- 5 files changed, 145 insertions(+), 25 deletions(-) create mode 100644 services/static-webserver/client/source/class/osparc/info/FunctionUtils.js diff --git a/services/static-webserver/client/source/class/osparc/data/Resources.js b/services/static-webserver/client/source/class/osparc/data/Resources.js index fda2bff08c9..582b919ec27 100644 --- a/services/static-webserver/client/source/class/osparc/data/Resources.js +++ b/services/static-webserver/client/source/class/osparc/data/Resources.js @@ -640,6 +640,10 @@ qx.Class.define("osparc.data.Resources", { method: "POST", url: statics.API + "/functions" }, + patch: { + method: "PATCH", + url: statics.API + "/functions/{functionId}" + }, } }, /* diff --git a/services/static-webserver/client/source/class/osparc/data/model/Function.js b/services/static-webserver/client/source/class/osparc/data/model/Function.js index 90599e5e153..84957c36756 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Function.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Function.js @@ -32,7 +32,7 @@ qx.Class.define("osparc.data.model.Function", { this.set({ uuid: functionData.uuid, functionClass: functionData.functionClass, - name: functionData.title, + title: functionData.title, description: functionData.description, inputSchema: functionData.inputSchema || this.getInputSchema(), outputSchema: functionData.outputSchema || this.getOutputSchema(), @@ -64,11 +64,11 @@ qx.Class.define("osparc.data.model.Function", { init: null }, - name: { + title: { check: "String", nullable: false, - event: "changeName", - init: "New Study" + event: "changeTitle", + init: "Function" }, description: { @@ -134,18 +134,27 @@ qx.Class.define("osparc.data.model.Function", { }, }, + statics: { + getProperties: function() { + return Object.keys(qx.util.PropertyUtil.getProperties(osparc.data.model.Function)); + } + }, + members: { - serialize: function(clean = true) { + serialize: function() { let jsonObject = {}; const propertyKeys = this.self().getProperties(); propertyKeys.forEach(key => { + if (key === "template") { + return; // template is not serialized + } jsonObject[key] = this.get(key); }); return jsonObject; }, patchFunction: function(functionChanges) { - return osparc.store.Study.getInstance().patchStudy(this.getUuid(), functionChanges) + return osparc.store.Functions.patchFunction(this.getUuid(), functionChanges) .then(() => { Object.keys(functionChanges).forEach(fieldKey => { const upKey = qx.lang.String.firstUp(fieldKey); @@ -156,9 +165,8 @@ qx.Class.define("osparc.data.model.Function", { lastChangeDate: new Date() }); const functionData = this.serialize(); - resolve(functionData); - }) - .catch(err => reject(err)); + return functionData; + }); }, } }); diff --git a/services/static-webserver/client/source/class/osparc/info/FunctionLarge.js b/services/static-webserver/client/source/class/osparc/info/FunctionLarge.js index 23307a35aab..a0bd999aff1 100644 --- a/services/static-webserver/client/source/class/osparc/info/FunctionLarge.js +++ b/services/static-webserver/client/source/class/osparc/info/FunctionLarge.js @@ -99,7 +99,7 @@ qx.Class.define("osparc.info.FunctionLarge", { const infoLayout = { "TITLE": { - view: osparc.info.StudyUtils.createTitle(this.getFunction()), + view: osparc.info.FunctionUtils.createTitle(this.getFunction()), action: { button: osparc.utils.Utils.getEditButton(canIWrite), callback: canIWrite ? this.__openTitleEditor : null, @@ -111,7 +111,7 @@ qx.Class.define("osparc.info.FunctionLarge", { action: null }, "DESCRIPTION": { - view: osparc.info.StudyUtils.createDescription(this.getFunction()), + view: osparc.info.FunctionUtils.createDescription(this.getFunction()), action: { button: osparc.utils.Utils.getEditButton(canIWrite), callback: canIWrite ? this.__openDescriptionEditor : null, @@ -120,17 +120,17 @@ qx.Class.define("osparc.info.FunctionLarge", { }, "ACCESS_RIGHTS": { label: this.tr("Permissions"), - view: new qx.ui.basic.Label(canIWrite ? "My Function" : "Read Only"), + view: osparc.info.FunctionUtils.createOwner(this.getFunction()), action: null }, "CREATED": { label: this.tr("Created"), - view: osparc.info.StudyUtils.createCreationDate(this.getFunction()), + view: osparc.info.FunctionUtils.createCreationDate(this.getFunction()), action: null }, "MODIFIED": { label: this.tr("Modified"), - view: osparc.info.StudyUtils.createLastChangeDate(this.getFunction()), + view: osparc.info.FunctionUtils.createLastChangeDate(this.getFunction()), action: null }, }; @@ -140,7 +140,7 @@ qx.Class.define("osparc.info.FunctionLarge", { __createThumbnail: function() { const maxWidth = 190; const maxHeight = 220; - const thumb = osparc.info.StudyUtils.createThumbnail(this.getFunction(), maxWidth, maxHeight); + const thumb = osparc.info.FunctionUtils.createThumbnail(this.getFunction(), maxWidth, maxHeight); thumb.set({ maxWidth: 120, maxHeight: 139 @@ -156,11 +156,11 @@ qx.Class.define("osparc.info.FunctionLarge", { __openTitleEditor: function() { const title = this.tr("Edit Title"); - const titleEditor = new osparc.widget.Renamer(this.getFunction().getName(), null, title); + const titleEditor = new osparc.widget.Renamer(this.getFunction().getTitle(), null, title); titleEditor.addListener("labelChanged", e => { titleEditor.close(); const newLabel = e.getData()["newLabel"]; - this.__patchFunction("name", newLabel); + this.__patchFunction("title", newLabel); }, this); titleEditor.center(); titleEditor.open(); @@ -187,10 +187,7 @@ qx.Class.define("osparc.info.FunctionLarge", { this.fireDataEvent("updateFunction", functionData); qx.event.message.Bus.getInstance().dispatchByName("updateFunction", functionData); }) - .catch(err => { - const msg = this.tr("An issue occurred while updating the information."); - osparc.FlashMessenger.logError(err, msg); - }); + .catch(err => osparc.FlashMessenger.logError(err)); } } }); diff --git a/services/static-webserver/client/source/class/osparc/info/FunctionUtils.js b/services/static-webserver/client/source/class/osparc/info/FunctionUtils.js new file mode 100644 index 00000000000..9f7c11e4217 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/info/FunctionUtils.js @@ -0,0 +1,103 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + + +qx.Class.define("osparc.info.FunctionUtils", { + type: "static", + + statics: { + /** + * @param func {osparc.data.model.Function} Function Model + */ + createTitle: function(func) { + const title = osparc.info.Utils.createTitle(); + func.bind("title", title, "value"); + return title; + }, + + /** + * @param func {osparc.data.model.Function} Function Model + * @param maxHeight {Number} description's maxHeight + */ + createDescription: function(func, maxHeight) { + const description = new osparc.ui.markdown.Markdown(); + func.bind("description", description, "value", { + converter: desc => desc ? desc : "No description" + }); + const scrollContainer = new qx.ui.container.Scroll(); + if (maxHeight) { + scrollContainer.setMaxHeight(maxHeight); + } + scrollContainer.add(description); + return scrollContainer; + }, + + /** + * @param func {osparc.data.model.Function} Function Model + */ + createOwner: function(func) { + const owner = new qx.ui.basic.Label(); + const canIWrite = func.getMyAccessRights()["write"]; + owner.setValue(canIWrite ? "My Function" : "Read Only"); + return owner; + }, + + /** + * @param func {osparc.data.model.Function} Function Model + */ + createCreationDate: function(func) { + const creationDate = new qx.ui.basic.Label(); + func.bind("creationDate", creationDate, "value", { + converter: date => osparc.utils.Utils.formatDateAndTime(date) + }); + return creationDate; + }, + + /** + * @param func {osparc.data.model.Function} Function Model + */ + createLastChangeDate: function(func) { + const lastChangeDate = new qx.ui.basic.Label(); + func.bind("lastChangeDate", lastChangeDate, "value", { + converter: date => osparc.utils.Utils.formatDateAndTime(date) + }); + return lastChangeDate; + }, + + /** + * @param func {osparc.data.model.Function} Function Model + * @param maxWidth {Number} thumbnail's maxWidth + * @param maxHeight {Number} thumbnail's maxHeight + */ + createThumbnail: function(func, maxWidth, maxHeight) { + const thumbnail = osparc.info.Utils.createThumbnail(maxWidth, maxHeight); + const noThumbnail = "osparc/no_photography_black_24dp.svg"; + func.bind("thumbnail", thumbnail, "source", { + converter: thumb => thumb ? thumb : noThumbnail, + onUpdate: (source, target) => { + if (source.getThumbnail() === "") { + target.getChildControl("image").set({ + minWidth: 120, + minHeight: 139 + }); + } + } + }); + return thumbnail; + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/store/Functions.js b/services/static-webserver/client/source/class/osparc/store/Functions.js index 7b489ac7537..a1e82f890b3 100644 --- a/services/static-webserver/client/source/class/osparc/store/Functions.js +++ b/services/static-webserver/client/source/class/osparc/store/Functions.js @@ -97,8 +97,7 @@ qx.Class.define("osparc.store.Functions", { const functions = response["data"]; functions.forEach(func => func["resourceType"] = "function"); return response; - }) - .catch(err => osparc.FlashMessenger.logError(err)); + }); }, fetchFunction: function(functionId) { @@ -111,8 +110,17 @@ qx.Class.define("osparc.store.Functions", { .then(func => { func["resourceType"] = "function"; return func; - }) - .catch(err => osparc.FlashMessenger.logError(err)); + }); + }, + + patchFunction: function(functionId, functionChanges) { + const params = { + url: { + functionId + }, + data: functionChanges + }; + return osparc.data.Resources.fetch("functions", "patch", params); }, invalidateFunctions: function() { From b59acb921e3a604160a4b47da788b7bfa5d92968 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Tue, 22 Jul 2025 15:29:12 +0200 Subject: [PATCH 59/69] back to accessRights --- .../client/source/class/osparc/data/model/Function.js | 10 +++++++--- .../client/source/class/osparc/info/FunctionLarge.js | 6 +----- .../client/source/class/osparc/info/FunctionUtils.js | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/data/model/Function.js b/services/static-webserver/client/source/class/osparc/data/model/Function.js index 84957c36756..026f7cb0903 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Function.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Function.js @@ -37,7 +37,7 @@ qx.Class.define("osparc.data.model.Function", { inputSchema: functionData.inputSchema || this.getInputSchema(), outputSchema: functionData.outputSchema || this.getOutputSchema(), defaultInputs: functionData.defaultInputs || this.getDefaultInputs(), - myAccessRights: functionData.accessRights || this.getMyAccessRights(), + accessRights: functionData.accessRights || this.getAccessRights(), creationDate: functionData.creationDate ? new Date(functionData.creationDate) : this.getCreationDate(), lastChangeDate: functionData.lastChangeDate ? new Date(functionData.lastChangeDate) : this.getLastChangeDate(), thumbnail: functionData.thumbnail || this.getThumbnail(), @@ -99,10 +99,10 @@ qx.Class.define("osparc.data.model.Function", { init: {} }, - myAccessRights: { + accessRights: { check: "Object", nullable: false, - event: "changeMyAccessRights", + event: "changeAccessRights", init: {} }, @@ -153,6 +153,10 @@ qx.Class.define("osparc.data.model.Function", { return jsonObject; }, + canIWrite: function() { + return Boolean(this.getAccessRights()["write"]); + }, + patchFunction: function(functionChanges) { return osparc.store.Functions.patchFunction(this.getUuid(), functionChanges) .then(() => { diff --git a/services/static-webserver/client/source/class/osparc/info/FunctionLarge.js b/services/static-webserver/client/source/class/osparc/info/FunctionLarge.js index a0bd999aff1..103dad7c5e8 100644 --- a/services/static-webserver/client/source/class/osparc/info/FunctionLarge.js +++ b/services/static-webserver/client/source/class/osparc/info/FunctionLarge.js @@ -45,10 +45,6 @@ qx.Class.define("osparc.info.FunctionLarge", { }, members: { - __canIWrite: function() { - return this.getFunction().getMyAccessRights()["write"]; - }, - _rebuildLayout: function() { this._removeAll(); @@ -95,7 +91,7 @@ qx.Class.define("osparc.info.FunctionLarge", { }, __infoElements: function() { - const canIWrite = this.__canIWrite(); + const canIWrite = this.getFunction().canIWrite(); const infoLayout = { "TITLE": { diff --git a/services/static-webserver/client/source/class/osparc/info/FunctionUtils.js b/services/static-webserver/client/source/class/osparc/info/FunctionUtils.js index 9f7c11e4217..d76c848cf80 100644 --- a/services/static-webserver/client/source/class/osparc/info/FunctionUtils.js +++ b/services/static-webserver/client/source/class/osparc/info/FunctionUtils.js @@ -51,7 +51,7 @@ qx.Class.define("osparc.info.FunctionUtils", { */ createOwner: function(func) { const owner = new qx.ui.basic.Label(); - const canIWrite = func.getMyAccessRights()["write"]; + const canIWrite = func.canIWrite(); owner.setValue(canIWrite ? "My Function" : "Read Only"); return owner; }, From 3b98ab44a97d32cc4df5b2643fc6e1bc4a4d4820 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Tue, 22 Jul 2025 15:43:08 +0200 Subject: [PATCH 60/69] keep templateId --- .../class/osparc/data/model/Function.js | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/data/model/Function.js b/services/static-webserver/client/source/class/osparc/data/model/Function.js index 026f7cb0903..4accce0a92f 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Function.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Function.js @@ -41,6 +41,7 @@ qx.Class.define("osparc.data.model.Function", { creationDate: functionData.creationDate ? new Date(functionData.creationDate) : this.getCreationDate(), lastChangeDate: functionData.lastChangeDate ? new Date(functionData.lastChangeDate) : this.getLastChangeDate(), thumbnail: functionData.thumbnail || this.getThumbnail(), + templateId: functionData.templateId || this.getTemplateId(), }); if (templateData) { @@ -127,6 +128,12 @@ qx.Class.define("osparc.data.model.Function", { init: null }, + templateId: { + check: "String", + nullable: true, + init: null, + }, + template: { check: "osparc.data.model.Study", nullable: true, @@ -158,19 +165,7 @@ qx.Class.define("osparc.data.model.Function", { }, patchFunction: function(functionChanges) { - return osparc.store.Functions.patchFunction(this.getUuid(), functionChanges) - .then(() => { - Object.keys(functionChanges).forEach(fieldKey => { - const upKey = qx.lang.String.firstUp(fieldKey); - const setter = "set" + upKey; - this[setter](functionChanges[fieldKey]); - }) - this.set({ - lastChangeDate: new Date() - }); - const functionData = this.serialize(); - return functionData; - }); + return osparc.store.Functions.patchFunction(this.getUuid(), functionChanges); }, } }); From d6488ae125a1808c68c948cdd61f4fe52a723516 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Tue, 22 Jul 2025 17:24:59 +0200 Subject: [PATCH 61/69] _add_extras_to_project_function --- .../functions/_controller/_functions_rest.py | 60 ++++++++++++++----- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index d0f661b86f6..9434bc9b2ff 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -9,8 +9,10 @@ ) from models_library.api_schemas_webserver.users import MyFunctionPermissionsGet from models_library.functions import FunctionClass, RegisteredProjectFunction +from models_library.products import ProductName from models_library.rest_pagination import Page from models_library.rest_pagination_utils import paginate_data +from models_library.users import UserID from pydantic import TypeAdapter from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( @@ -37,6 +39,32 @@ routes = web.RouteTableDef() +async def _add_extras_to_project_function( + function: RegisteredProjectFunction, + app: web.Application, + user_id: UserID, + product_name: ProductName, +) -> dict: + assert isinstance(function, RegisteredProjectFunction) # nosec + + project_dict = await _projects_service.get_project_for_user( + app=app, + project_uuid=f"{function.project_id}", + user_id=user_id, + ) + + function_with_extras = function.model_dump(mode="json") | { + "access_rights": await _functions_service.get_function_user_permissions( + app, + user_id=user_id, + product_name=product_name, + function_id=function.uid, + ), + "thumbnail": project_dict.get("thumbnail", None), + } + return function_with_extras + + @routes.post(f"/{VTAG}/functions", name="register_function") @login_required @handle_rest_requests_exceptions @@ -177,26 +205,16 @@ async def get_function(request: web.Request) -> web.Response: query_params.include_extras and registered_function.function_class == FunctionClass.PROJECT ): - assert isinstance(registered_function, RegisteredProjectFunctionGet) # nosec - - project_dict = await _projects_service.get_project_for_user( + function_with_extras = await _add_extras_to_project_function( + function=registered_function, app=request.app, - project_uuid=f"{registered_function.project_id}", user_id=req_ctx.user_id, + product_name=req_ctx.product_name, ) return envelope_json_response( TypeAdapter(RegisteredProjectFunctionGet).validate_python( - registered_function.model_dump(mode="json") - | { - "access_rights": await _functions_service.get_function_user_permissions( - request.app, - user_id=req_ctx.user_id, - product_name=req_ctx.product_name, - function_id=function_id, - ), - "thumbnail": project_dict.get("thumbnail", None), - } + function_with_extras ) ) @@ -231,6 +249,20 @@ async def update_function(request: web.Request) -> web.Response: function=function_update, ) + if updated_function.function_class == FunctionClass.PROJECT: + function_with_extras = await _add_extras_to_project_function( + function=updated_function, + app=request.app, + user_id=req_ctx.user_id, + product_name=req_ctx.product_name, + ) + + return envelope_json_response( + TypeAdapter(RegisteredProjectFunctionGet).validate_python( + function_with_extras + ) + ) + return envelope_json_response( TypeAdapter(RegisteredFunctionGet).validate_python( updated_function.model_dump(mode="json") From aba75de95a62eedcefa7f70bb47abfef2ed3c7f8 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 23 Jul 2025 10:31:31 +0200 Subject: [PATCH 62/69] patchFunction --- .../client/source/class/osparc/data/model/Function.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/services/static-webserver/client/source/class/osparc/data/model/Function.js b/services/static-webserver/client/source/class/osparc/data/model/Function.js index 4accce0a92f..bd4da8a9d54 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Function.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Function.js @@ -165,7 +165,15 @@ qx.Class.define("osparc.data.model.Function", { }, patchFunction: function(functionChanges) { - return osparc.store.Functions.patchFunction(this.getUuid(), functionChanges); + return osparc.store.Functions.patchFunction(this.getUuid(), functionChanges) + .then(functionData => { + Object.keys(functionChanges).forEach(fieldKey => { + const upKey = qx.lang.String.firstUp(fieldKey); + const setter = "set" + upKey; + this[setter](functionChanges[fieldKey]); + }); + return functionData; + }); }, } }); From 3f2f3f1fccd58dec269c9dc29991b92df5e10f57 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 23 Jul 2025 10:35:19 +0200 Subject: [PATCH 63/69] minor --- .../client/source/class/osparc/data/model/Function.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/static-webserver/client/source/class/osparc/data/model/Function.js b/services/static-webserver/client/source/class/osparc/data/model/Function.js index bd4da8a9d54..6a98bdccc33 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Function.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Function.js @@ -172,6 +172,9 @@ qx.Class.define("osparc.data.model.Function", { const setter = "set" + upKey; this[setter](functionChanges[fieldKey]); }); + this.set({ + lastChangeDate: new Date(functionData.lastChangeDate) + }); return functionData; }); }, From b8e59891e8785e0cda659af67edc1ad27a8ab223 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 23 Jul 2025 10:40:13 +0200 Subject: [PATCH 64/69] no magic strings --- .../source/class/osparc/dashboard/CardBase.js | 6 +++++- .../class/osparc/dashboard/ResourceDetails.js | 4 ++-- .../source/class/osparc/data/model/Function.js | 14 ++++++++++++-- .../client/source/class/osparc/store/Functions.js | 2 +- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js b/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js index e8f378aaea3..23938c97fe3 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js @@ -597,7 +597,11 @@ qx.Class.define("osparc.dashboard.CardBase", { break; } case "function": - this.setIcon(osparc.data.model.StudyUI.PIPELINE_ICON); + if (resourceData["functionClass"] === osparc.data.model.Function.FUNCTION_CLASS.PROJECT) { + this.setIcon(osparc.data.model.StudyUI.PIPELINE_ICON); + } else { + this.setIcon(osparc.dashboard.CardBase.PRODUCT_ICON); + } break; } }, diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js index 517cd864fa9..5855044dc11 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js @@ -81,7 +81,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { this.__resourceModel["resourceType"] = resourceData["resourceType"]; this.__addPages(); } - if (resourceData["functionClass"] === "PROJECT") { + if (resourceData["functionClass"] === osparc.data.model.Function.FUNCTION_CLASS.PROJECT) { // this is only required for functions that have a template linked osparc.store.Templates.fetchTemplate(resourceData["templateId"]) .then(templateData => { @@ -408,7 +408,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { return; } else if (osparc.utils.Resources.isFunction(this.__resourceData)) { this.__addInfoPage(); - if (this.__resourceModel.getFunctionClass() === "PROJECT") { + if (this.__resourceModel.getFunctionClass() === osparc.data.model.Function.FUNCTION_CLASS.PROJECT) { this.__addPreviewPage(); } this.fireEvent("pagesAdded"); diff --git a/services/static-webserver/client/source/class/osparc/data/model/Function.js b/services/static-webserver/client/source/class/osparc/data/model/Function.js index 6a98bdccc33..fa1028ae4a0 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Function.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Function.js @@ -59,7 +59,11 @@ qx.Class.define("osparc.data.model.Function", { }, functionClass: { - check: ["PROJECT", "SOLVER", "PYTHON_CODE"], + check: [ + "PROJECT", // osparc.data.model.Function.FUNCTION_CLASS.PROJECT + "SOLVER", // osparc.data.model.Function.FUNCTION_CLASS.SOLVER + "PYTHON_CODE", // osparc.data.model.Function.FUNCTION_CLASS.PYTHON + ], nullable: false, event: "changeFunctionClass", init: null @@ -142,9 +146,15 @@ qx.Class.define("osparc.data.model.Function", { }, statics: { + FUNCTION_CLASS: { + PROJECT: "PROJECT", + SOLVER: "SOLVER", + PYTHON_CODE: "PYTHON_CODE" + }, + getProperties: function() { return Object.keys(qx.util.PropertyUtil.getProperties(osparc.data.model.Function)); - } + }, }, members: { diff --git a/services/static-webserver/client/source/class/osparc/store/Functions.js b/services/static-webserver/client/source/class/osparc/store/Functions.js index a1e82f890b3..2d5db21466e 100644 --- a/services/static-webserver/client/source/class/osparc/store/Functions.js +++ b/services/static-webserver/client/source/class/osparc/store/Functions.js @@ -27,7 +27,7 @@ qx.Class.define("osparc.store.Functions", { "projectId": templateData["uuid"], "title": name, "description": description, - "function_class": "PROJECT", + "function_class": osparc.data.model.Function.FUNCTION_CLASS.PROJECT, "inputSchema": { "schema_class": "application/schema+json", "schema_content": { From 9086b3b8c5ead5e97208a3a9988688190a839826 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 23 Jul 2025 10:52:21 +0200 Subject: [PATCH 65/69] shared-icon --- .../source/class/osparc/dashboard/CardBase.js | 8 +++++++ .../class/osparc/dashboard/GridButtonItem.js | 21 ++++++++++++------- .../class/osparc/dashboard/ListButtonItem.js | 21 ++++++++++++------- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js b/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js index 23938c97fe3..ea85b74a8a4 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js @@ -228,6 +228,14 @@ qx.Class.define("osparc.dashboard.CardBase", { this.addHintFromGids(shareIcon, gids); }, + populateMyAccessRightsIcon: function(shareIcon, myAccessRights) { + const canIWrite = Boolean(myAccessRights["write"]); + shareIcon.set({ + source: canIWrite ? osparc.dashboard.CardBase.SHARE_ICON : osparc.dashboard.CardBase.SHARED_USER, + toolTipText: canIWrite ? "" : qx.locale.Manager.tr("Shared"), + }); + }, + addHintFromGids: function(icon, gids) { const groupsStore = osparc.store.Groups.getInstance(); const groupEveryone = groupsStore.getEveryoneGroup(); diff --git a/services/static-webserver/client/source/class/osparc/dashboard/GridButtonItem.js b/services/static-webserver/client/source/class/osparc/dashboard/GridButtonItem.js index e9fbc461413..1acb22a5c21 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/GridButtonItem.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/GridButtonItem.js @@ -262,15 +262,20 @@ qx.Class.define("osparc.dashboard.GridButtonItem", { _applyAccessRights: function(value) { if (value && Object.keys(value).length) { const shareIcon = this.getChildControl("subtitle-icon"); - shareIcon.addListener("tap", e => { - e.stopPropagation(); - this.openAccessRights(); - }, this); - shareIcon.addListener("pointerdown", e => e.stopPropagation()); - osparc.dashboard.CardBase.populateShareIcon(shareIcon, value); + if (this.isResourceType("function")) { + // in case of functions, the access rights are actually myAccessRights + osparc.dashboard.CardBase.populateMyAccessRightsIcon(shareIcon, value); + } else { + shareIcon.addListener("tap", e => { + e.stopPropagation(); + this.openAccessRights(); + }, this); + shareIcon.addListener("pointerdown", e => e.stopPropagation()); + osparc.dashboard.CardBase.populateShareIcon(shareIcon, value); - if (this.isResourceType("study")) { - this._setStudyPermissions(value); + if (this.isResourceType("study")) { + this._setStudyPermissions(value); + } } } }, diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ListButtonItem.js b/services/static-webserver/client/source/class/osparc/dashboard/ListButtonItem.js index f8cd7c13dfa..2768b1cf589 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ListButtonItem.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ListButtonItem.js @@ -257,15 +257,20 @@ qx.Class.define("osparc.dashboard.ListButtonItem", { _applyAccessRights: function(value) { if (value && Object.keys(value).length) { const shareIcon = this.getChildControl("shared-icon"); - shareIcon.addListener("tap", e => { - e.stopPropagation(); - this.openAccessRights(); - }, this); - shareIcon.addListener("pointerdown", e => e.stopPropagation()); - osparc.dashboard.CardBase.populateShareIcon(shareIcon, value); + if (this.isResourceType("function")) { + // in case of functions, the access rights are actually myAccessRights + osparc.dashboard.CardBase.populateMyAccessRightsIcon(shareIcon, value); + } else { + shareIcon.addListener("tap", e => { + e.stopPropagation(); + this.openAccessRights(); + }, this); + shareIcon.addListener("pointerdown", e => e.stopPropagation()); + osparc.dashboard.CardBase.populateShareIcon(shareIcon, value); - if (this.isResourceType("study")) { - this._setStudyPermissions(value); + if (this.isResourceType("study")) { + this._setStudyPermissions(value); + } } } }, From 07b68e69ae98851e594771361c8d9a865d936f18 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 23 Jul 2025 11:04:30 +0200 Subject: [PATCH 66/69] robot is right --- .../client/source/class/osparc/po/UsersPending.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/po/UsersPending.js b/services/static-webserver/client/source/class/osparc/po/UsersPending.js index 0da1fa091a7..9e73a8cfedb 100644 --- a/services/static-webserver/client/source/class/osparc/po/UsersPending.js +++ b/services/static-webserver/client/source/class/osparc/po/UsersPending.js @@ -179,7 +179,7 @@ qx.Class.define("osparc.po.UsersPending", { date = pendingUser.accountRequestReviewedAt ? osparc.utils.Utils.formatDateAndTime(new Date(pendingUser.accountRequestReviewedAt)) : "-"; break; } - pendingUsersLayout.add(date, { + pendingUsersLayout.add(new qx.ui.basic.Label(date), { row, column: 2, }); @@ -248,13 +248,13 @@ qx.Class.define("osparc.po.UsersPending", { const sortByDate = (a, b) => { let dateA = new Date(0); // default to epoch if no date is available if (a.accountRequestStatus === "PENDING" && a.preRegistrationRequestedAt) { - dateA = new Date(a.preRegistrationRequestedAt) + dateA = new Date(a.preRegistrationRequestedAt); } else if (a.accountRequestReviewedAt) { dateA = new Date(a.accountRequestReviewedAt); } let dateB = new Date(0); // default to epoch if no date is available if (b.accountRequestStatus === "PENDING" && b.preRegistrationRequestedAt) { - dateB = new Date(b.preRegistrationRequestedAt) + dateB = new Date(b.preRegistrationRequestedAt); } else if (b.accountRequestReviewedAt) { dateB = new Date(b.accountRequestReviewedAt); } From 4f01370ff3c7610a8fefbb8ed96ed9dd898897d5 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 23 Jul 2025 11:06:47 +0200 Subject: [PATCH 67/69] better practices --- .../client/source/class/osparc/store/Functions.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/services/static-webserver/client/source/class/osparc/store/Functions.js b/services/static-webserver/client/source/class/osparc/store/Functions.js index 2d5db21466e..606e074c79c 100644 --- a/services/static-webserver/client/source/class/osparc/store/Functions.js +++ b/services/static-webserver/client/source/class/osparc/store/Functions.js @@ -110,6 +110,10 @@ qx.Class.define("osparc.store.Functions", { .then(func => { func["resourceType"] = "function"; return func; + }) + .catch(error => { + console.error("Error fetching function:", error); + throw error; // Rethrow the error to propagate it to the caller }); }, @@ -120,7 +124,11 @@ qx.Class.define("osparc.store.Functions", { }, data: functionChanges }; - return osparc.data.Resources.fetch("functions", "patch", params); + return osparc.data.Resources.fetch("functions", "patch", params) + .catch(error => { + console.error("Error patching function:", error); + throw error; // Rethrow the error to propagate it to the caller + }); }, invalidateFunctions: function() { From 965edb059beb22773592a81f8f4c88efba80ba0a Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Wed, 23 Jul 2025 12:49:17 +0200 Subject: [PATCH 68/69] fix: enrich when add extras is requested --- .../functions/_controller/_functions_rest.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 9434bc9b2ff..d450ef5f5f1 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -236,6 +236,10 @@ async def update_function(request: web.Request) -> web.Response: path_params = parse_request_path_parameters_as(FunctionPathParams, request) function_id = path_params.function_id + query_params: FunctionGetQueryParams = parse_request_query_parameters_as( + FunctionGetQueryParams, request + ) + function_update = TypeAdapter(RegisteredFunctionUpdate).validate_python( await request.json() ) @@ -249,7 +253,10 @@ async def update_function(request: web.Request) -> web.Response: function=function_update, ) - if updated_function.function_class == FunctionClass.PROJECT: + if ( + query_params.include_extras + and updated_function.function_class == FunctionClass.PROJECT + ): function_with_extras = await _add_extras_to_project_function( function=updated_function, app=request.app, From f8ad103f7b7a6a58f009cb0b13d5749516f66606 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Wed, 23 Jul 2025 13:35:06 +0200 Subject: [PATCH 69/69] fix: add include_extras query param --- .../client/source/class/osparc/data/Resources.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/static-webserver/client/source/class/osparc/data/Resources.js b/services/static-webserver/client/source/class/osparc/data/Resources.js index 582b919ec27..77789fdcbc2 100644 --- a/services/static-webserver/client/source/class/osparc/data/Resources.js +++ b/services/static-webserver/client/source/class/osparc/data/Resources.js @@ -642,7 +642,7 @@ qx.Class.define("osparc.data.Resources", { }, patch: { method: "PATCH", - url: statics.API + "/functions/{functionId}" + url: statics.API + "/functions/{functionId}?include_extras=true" }, } },