From d9fd2dab9121c0cddedfb6d6b1456e786eaa3ce6 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 11 May 2023 14:56:44 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Project=20list=20items=20include?= =?UTF-8?q?=20permalink=20(#4214)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...es_dynamic_sidecar_docker_service_specs.py | 1 - .../source/class/osparc/data/model/Study.js | 8 ++ .../projects/_read_utils.py | 100 ++++++++++++++++++ .../projects/projects_handlers_crud.py | 46 ++------ .../02/test_projects_handlers_crud.py | 22 +++- .../test_meta_modeling_iterations.py | 1 - 6 files changed, 137 insertions(+), 41 deletions(-) create mode 100644 services/web/server/src/simcore_service_webserver/projects/_read_utils.py diff --git a/services/director-v2/tests/unit/with_dbs/test_modules_dynamic_sidecar_docker_service_specs.py b/services/director-v2/tests/unit/with_dbs/test_modules_dynamic_sidecar_docker_service_specs.py index 531814fc966..1b7fa46c27b 100644 --- a/services/director-v2/tests/unit/with_dbs/test_modules_dynamic_sidecar_docker_service_specs.py +++ b/services/director-v2/tests/unit/with_dbs/test_modules_dynamic_sidecar_docker_service_specs.py @@ -421,7 +421,6 @@ def test_get_dynamic_proxy_spec( # TODO: finish test when working on https://github.com/ITISFoundation/osparc-simcore/issues/2454 -@pytest.mark.testit async def test_merge_dynamic_sidecar_specs_with_user_specific_specs( mocked_catalog_service_api: respx.MockRouter, minimal_app: FastAPI, diff --git a/services/static-webserver/client/source/class/osparc/data/model/Study.js b/services/static-webserver/client/source/class/osparc/data/model/Study.js index db1d19ffd70..229f92e586b 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Study.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Study.js @@ -55,6 +55,7 @@ qx.Class.define("osparc.data.model.Study", { tags: studyData.tags || this.getTags(), state: studyData.state || this.getState(), quality: studyData.quality || this.getQuality(), + permalink: studyData.permalink || this.getPermalink(), dev: studyData.dev || this.getDev() }); @@ -156,6 +157,12 @@ qx.Class.define("osparc.data.model.Study", { nullable: true }, + permalink: { + check: "Object", + nullable: true, + init: {} + }, + dev: { check: "Object", nullable: true, @@ -188,6 +195,7 @@ qx.Class.define("osparc.data.model.Study", { statics: { IgnoreSerializationProps: [ + "permalink", "state", "pipelineRunning", "readOnly" diff --git a/services/web/server/src/simcore_service_webserver/projects/_read_utils.py b/services/web/server/src/simcore_service_webserver/projects/_read_utils.py new file mode 100644 index 00000000000..e6d057a5d4a --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/projects/_read_utils.py @@ -0,0 +1,100 @@ +""" Utils to implement READ operations (from cRud) on the project resource + + +Read operations are list, get + +""" +from aiohttp import web +from models_library.projects import ProjectID +from models_library.users import UserID +from pydantic import NonNegativeInt +from servicelib.utils import logged_gather +from simcore_postgres_database.webserver_models import ProjectType as ProjectTypeDB +from simcore_service_webserver.rest_schemas_base import OutputSchema + +from .. import catalog +from . import projects_api +from ._permalink import update_or_pop_permalink_in_project +from ._rest_schemas import ProjectListItem +from .project_models import ProjectDict, ProjectTypeAPI +from .projects_db import ProjectDBAPI + + +async def _append_fields( + request: web.Request, + user_id: UserID, + project: ProjectDict, + is_template: bool, + model_schema_cls: type[OutputSchema], +): + # state + await projects_api.add_project_states_for_user( + user_id=user_id, + project=project, + is_template=is_template, + app=request.app, + ) + + # permalink + await update_or_pop_permalink_in_project(request, project) + + # validate + project_data = model_schema_cls.parse_obj(project).data(exclude_unset=True) + return project_data + + +async def list_projects( + request: web.Request, + user_id: UserID, + product_name: str, + project_type: ProjectTypeAPI, + show_hidden: bool, + offset: NonNegativeInt, + limit: int, +) -> tuple[list[ProjectDict], int]: + + app = request.app + db = ProjectDBAPI.get_from_app_context(app) + + user_available_services: list[ + dict + ] = await catalog.get_services_for_user_in_product( + app, user_id, product_name, only_key_versions=True + ) + + db_projects, db_project_types, total_number_projects = await db.list_projects( + user_id=user_id, + product_name=product_name, + filter_by_project_type=ProjectTypeAPI.to_project_type_db(project_type), + filter_by_services=user_available_services, + offset=offset, + limit=limit, + include_hidden=show_hidden, + ) + + projects: list[ProjectDict] = await logged_gather( + *( + _append_fields( + request, + user_id, + project=prj, + is_template=prj_type == ProjectTypeDB.TEMPLATE, + model_schema_cls=ProjectListItem, + ) + for prj, prj_type in zip(db_projects, db_project_types) + ), + reraise=True, + max_concurrency=100, + ) + + return projects, total_number_projects + + +async def get_project( + request: web.Request, + user_id: UserID, + product_name: str, + project_uuid: ProjectID, + project_type: ProjectTypeAPI, +): + raise NotImplementedError() diff --git a/services/web/server/src/simcore_service_webserver/projects/projects_handlers_crud.py b/services/web/server/src/simcore_service_webserver/projects/projects_handlers_crud.py index a25fd62aed0..df7b3973df9 100644 --- a/services/web/server/src/simcore_service_webserver/projects/projects_handlers_crud.py +++ b/services/web/server/src/simcore_service_webserver/projects/projects_handlers_crud.py @@ -5,7 +5,6 @@ """ import json import logging -from typing import Any from aiohttp import web from jsonschema import ValidationError as JsonSchemaValidationError @@ -29,8 +28,6 @@ from servicelib.json_serialization import json_dumps from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON from servicelib.rest_constants import RESPONSE_MODEL_POLICY -from servicelib.utils import logged_gather -from simcore_postgres_database.webserver_models import ProjectType as ProjectTypeDB from .. import catalog from .._constants import RQ_PRODUCT_KEY @@ -41,7 +38,7 @@ from ..security.api import check_permission from ..security.decorators import permission_required from ..users_api import get_user_name -from . import _create_utils, projects_api +from . import _create_utils, _read_utils, projects_api from ._permalink import update_or_pop_permalink_in_project from ._rest_schemas import ( EmptyModel, @@ -203,51 +200,24 @@ async def list_projects(request: web.Request): web.HTTPUnprocessableEntity: (422) if validation of request parameters fail """ - - db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(request.app) req_ctx = RequestContext.parse_obj(request) query_params = parse_request_query_parameters_as(_ProjectListParams, request) - async def set_all_project_states( - projects: list[dict[str, Any]], project_types: list[ProjectTypeDB] - ): - await logged_gather( - *[ - projects_api.add_project_states_for_user( - user_id=req_ctx.user_id, - project=prj, - is_template=prj_type == ProjectTypeDB.TEMPLATE, - app=request.app, - ) - for prj, prj_type in zip(projects, project_types) - ], - reraise=True, - max_concurrency=100, - ) - - user_available_services: list[ - dict - ] = await catalog.get_services_for_user_in_product( - request.app, req_ctx.user_id, req_ctx.product_name, only_key_versions=True - ) - - projects, project_types, total_number_projects = await db.list_projects( + projects, total_number_of_projects = await _read_utils.list_projects( + request, user_id=req_ctx.user_id, product_name=req_ctx.product_name, - filter_by_project_type=ProjectTypeAPI.to_project_type_db( - query_params.project_type - ), - filter_by_services=user_available_services, - offset=query_params.offset, + project_type=query_params.project_type, + show_hidden=query_params.show_hidden, limit=query_params.limit, - include_hidden=query_params.show_hidden, + offset=query_params.offset, ) - await set_all_project_states(projects, project_types) + page = Page[ProjectDict].parse_obj( paginate_data( chunk=projects, request_url=request.url, - total=total_number_projects, + total=total_number_of_projects, limit=query_params.limit, offset=query_params.offset, ) diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_handlers_crud.py b/services/web/server/tests/unit/with_dbs/02/test_projects_handlers_crud.py index 6a3274a8d92..9f5f3e241a4 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_handlers_crud.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_handlers_crud.py @@ -158,12 +158,14 @@ async def _assert_get_same_project( async def _replace_project( client: TestClient, project_update: dict, expected: type[web.HTTPException] ) -> dict: + assert client.app + # PUT /v0/projects/{project_id} url = client.app.router["replace_project"].url_for( project_id=project_update["uuid"] ) assert str(url) == f"{API_PREFIX}/projects/{project_update['uuid']}" - resp = await client.put(url, json=project_update) + resp = await client.put(f"{url}", json=project_update) data, error = await assert_status(resp, expected) if not error: assert_replaced(current_project=data, update_data=project_update) @@ -194,36 +196,54 @@ async def test_list_projects( if data: assert len(data) == 2 + # template project project_state = data[0].pop("state") + project_permalink = data[0].pop("permalink") + assert data[0] == template_project assert not ProjectState( **project_state ).locked.value, "Templates are not locked" + assert parse_obj_as(ProjectPermalink, project_permalink) + # standard project project_state = data[1].pop("state") + project_permalink = data[1].pop("permalink", None) + assert data[1] == user_project assert ProjectState(**project_state) + assert project_permalink is None # GET /v0/projects?type=user data, *_ = await _list_projects(client, expected, {"type": "user"}) if data: assert len(data) == 1 + + # standad project project_state = data[0].pop("state") + project_permalink = data[0].pop("permalink", None) + assert data[0] == user_project assert not ProjectState( **project_state ).locked.value, "Single user does not lock" + assert project_permalink is None # GET /v0/projects?type=template # instead /v0/projects/templates ?? data, *_ = await _list_projects(client, expected, {"type": "template"}) if data: assert len(data) == 1 + + # template project project_state = data[0].pop("state") + project_permalink = data[0].pop("permalink") + assert data[0] == template_project assert not ProjectState( **project_state ).locked.value, "Templates are not locked" + assert parse_obj_as(ProjectPermalink, project_permalink) @pytest.fixture(scope="session") diff --git a/services/web/server/tests/unit/with_dbs/03/meta_modeling/test_meta_modeling_iterations.py b/services/web/server/tests/unit/with_dbs/03/meta_modeling/test_meta_modeling_iterations.py index b6a8bf50ebd..efb03fbd190 100644 --- a/services/web/server/tests/unit/with_dbs/03/meta_modeling/test_meta_modeling_iterations.py +++ b/services/web/server/tests/unit/with_dbs/03/meta_modeling/test_meta_modeling_iterations.py @@ -67,7 +67,6 @@ async def context_with_logged_user(client: TestClient, logged_user: UserInfoDict await conn.execute(projects.delete()) -@pytest.mark.testit @pytest.mark.acceptance_test async def test_iterators_workflow( client: TestClient,