From ce0a6064a9850f84a65cd3f199a65c777d5b6151 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:00:04 +0100 Subject: [PATCH 01/50] drafts test --- .../api_schemas_webserver/users.py | 2 +- .../users/_users_rest.py | 5 +- .../tests/unit/with_dbs/03/test_users.py | 123 +++++++++++++----- 3 files changed, 94 insertions(+), 36 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 341e422a50e6..947623fc9567 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 @@ -215,7 +215,7 @@ class UserGet(OutputSchema): # Public profile of a user subject to its privacy settings user_id: UserID group_id: GroupID - user_name: UserNameID + user_name: UserNameID | None = None first_name: str | None = None last_name: str | None = None email: EmailStr | None = None diff --git a/services/web/server/src/simcore_service_webserver/users/_users_rest.py b/services/web/server/src/simcore_service_webserver/users/_users_rest.py index 6b3f00ac1b07..e3c143cd7cf4 100644 --- a/services/web/server/src/simcore_service_webserver/users/_users_rest.py +++ b/services/web/server/src/simcore_service_webserver/users/_users_rest.py @@ -180,7 +180,10 @@ async def search_users_for_admin(request: web.Request) -> web.Response: ) return envelope_json_response( - [_.model_dump(**_RESPONSE_MODEL_MINIMAL_POLICY) for _ in found] + [ + user_for_admin.model_dump(**_RESPONSE_MODEL_MINIMAL_POLICY) + for user_for_admin in found + ] ) diff --git a/services/web/server/tests/unit/with_dbs/03/test_users.py b/services/web/server/tests/unit/with_dbs/03/test_users.py index 6b0ba408cc0d..f5b453bbe254 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_users.py +++ b/services/web/server/tests/unit/with_dbs/03/test_users.py @@ -62,15 +62,36 @@ def app_environment( @pytest.fixture -async def private_user(client: TestClient) -> AsyncIterable[UserInfoDict]: +def partial_first_name() -> str: + return "James" + + +@pytest.fixture +def partial_username() -> str: + return "COMMON_USERNAME" + + +@pytest.fixture +def partial_email() -> str: + return "@acme.com" + + +@pytest.fixture +async def private_user( + client: TestClient, + partial_username: str, + partial_email: str, + partial_first_name: str, +) -> AsyncIterable[UserInfoDict]: assert client.app async with NewUser( app=client.app, user_data={ - "name": "jamie01", - "first_name": "James", + "name": f"james{partial_username}", + "first_name": partial_first_name, "last_name": "Bond", - "email": "james@find.me", + "email": f"james{partial_email}", + "privacy_hide_username": True, "privacy_hide_email": True, "privacy_hide_fullname": True, }, @@ -79,15 +100,18 @@ async def private_user(client: TestClient) -> AsyncIterable[UserInfoDict]: @pytest.fixture -async def semi_private_user(client: TestClient) -> AsyncIterable[UserInfoDict]: +async def semi_private_user( + client: TestClient, partial_username: str, partial_first_name: str +) -> AsyncIterable[UserInfoDict]: assert client.app async with NewUser( app=client.app, user_data={ - "name": "maxwell", - "first_name": "James", + "name": f"maxwell{partial_username}", + "first_name": partial_first_name, "last_name": "Maxwell", "email": "j@maxwell.me", + "privacy_hide_username": False, "privacy_hide_email": True, "privacy_hide_fullname": False, # <-- }, @@ -96,15 +120,18 @@ async def semi_private_user(client: TestClient) -> AsyncIterable[UserInfoDict]: @pytest.fixture -async def public_user(client: TestClient) -> AsyncIterable[UserInfoDict]: +async def public_user( + client: TestClient, partial_username: str, partial_email: str +) -> AsyncIterable[UserInfoDict]: assert client.app async with NewUser( app=client.app, user_data={ - "name": "taylie01", + "name": f"taylor{partial_username}", "first_name": "Taylor", "last_name": "Swift", - "email": "taylor@find.me", + "email": f"taylor{partial_email}", + "privacy_hide_username": False, "privacy_hide_email": False, "privacy_hide_fullname": False, }, @@ -112,14 +139,12 @@ async def public_user(client: TestClient) -> AsyncIterable[UserInfoDict]: yield usr -@pytest.mark.acceptance_test( - "https://github.com/ITISFoundation/osparc-issues/issues/1779" -) @pytest.mark.parametrize("user_role", [UserRole.USER]) -async def test_search_users( +async def test_search_users_by_partial_fullname( + user_role: UserRole, logged_user: UserInfoDict, client: TestClient, - user_role: UserRole, + partial_first_name: str, public_user: UserInfoDict, semi_private_user: UserInfoDict, private_user: UserInfoDict, @@ -127,29 +152,43 @@ async def test_search_users( assert client.app assert user_role.value == logged_user["role"] + # logged_user has default settings assert private_user["id"] != logged_user["id"] assert public_user["id"] != logged_user["id"] # SEARCH by partial first_name - partial_name = "james" - assert partial_name in private_user.get("first_name", "").lower() - assert partial_name in semi_private_user.get("first_name", "").lower() + assert partial_first_name in private_user.get("first_name", "") + assert partial_first_name in semi_private_user.get("first_name", "") + assert partial_first_name not in public_user.get("first_name", "") url = client.app.router["search_users"].url_for() - resp = await client.post(f"{url}", json={"match": partial_name}) + resp = await client.post(f"{url}", json={"match": partial_first_name}) data, _ = await assert_status(resp, status.HTTP_200_OK) + # expected `semi_private_user` found found = TypeAdapter(list[UserGet]).validate_python(data) assert found assert len(found) == 1 - assert semi_private_user["name"] == found[0].user_name + assert found[0].user_name == semi_private_user["name"] assert found[0].first_name == semi_private_user.get("first_name") assert found[0].last_name == semi_private_user.get("last_name") assert found[0].email is None + +@pytest.mark.parametrize("user_role", [UserRole.USER]) +async def test_search_users_by_partial_email( + logged_user: UserInfoDict, + client: TestClient, + user_role: UserRole, + partial_email: str, + public_user: UserInfoDict, + semi_private_user: UserInfoDict, + private_user: UserInfoDict, +): + # SEARCH by partial email - partial_email = "@find.m" assert partial_email in private_user["email"] + assert partial_email not in semi_private_user["email"] assert partial_email in public_user["email"] url = client.app.router["search_users"].url_for() @@ -159,15 +198,36 @@ async def test_search_users( found = TypeAdapter(list[UserGet]).validate_python(data) assert found assert len(found) == 1 + + # expected `public_user` found assert found[0].user_id == public_user["id"] assert found[0].user_name == public_user["name"] assert found[0].email == public_user["email"] assert found[0].first_name == public_user.get("first_name") assert found[0].last_name == public_user.get("last_name") + # SEARCH user for admin (from a USER) + url = ( + client.app.router["search_users_for_admin"] + .url_for() + .with_query(email=partial_email) + ) + resp = await client.get(f"{url}") + await assert_status(resp, status.HTTP_403_FORBIDDEN) + + +async def test_search_users_by_partial_username( + logged_user: UserInfoDict, + client: TestClient, + partial_username: str, + user_role: UserRole, + public_user: UserInfoDict, + semi_private_user: UserInfoDict, + private_user: UserInfoDict, +): # SEARCH by partial username - partial_username = "ie01" assert partial_username in private_user["name"] + assert partial_username in semi_private_user["name"] assert partial_username in public_user["name"] url = client.app.router["search_users"].url_for() @@ -178,25 +238,20 @@ async def test_search_users( assert found assert len(found) == 2 + # expected `public_user` found index = [u.user_id for u in found].index(public_user["id"]) assert found[index].user_name == public_user["name"] + assert found[index].email == public_user["email"] + assert found[index].first_name == public_user.get("first_name") + assert found[index].last_name == public_user.get("last_name") - # check privacy + # expected `semi_private_user` found index = (index + 1) % 2 - assert found[index].user_name == private_user["name"] + assert found[index].user_name == semi_private_user["name"] assert found[index].email is None assert found[index].first_name is None assert found[index].last_name is None - # SEARCH user for admin (from a USER) - url = ( - client.app.router["search_users_for_admin"] - .url_for() - .with_query(email=partial_email) - ) - resp = await client.get(f"{url}") - await assert_status(resp, status.HTTP_403_FORBIDDEN) - @pytest.mark.acceptance_test( "https://github.com/ITISFoundation/osparc-issues/issues/1779" @@ -699,7 +754,7 @@ def test_preuserprofile_parse_model_from_request_form_data( def test_preuserprofile_parse_model_without_extras( - account_request_form: dict[str, Any] + account_request_form: dict[str, Any], ): required = { f.alias or f_name From d5f2e7eb16865aad1c8bcb5cfd2c38585b5cbaa7 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:08:08 +0100 Subject: [PATCH 02/50] privacy --- .../src/simcore_postgres_database/models/users.py | 7 +++++++ .../src/simcore_postgres_database/utils_users.py | 9 ++++++++- .../groups/_groups_repository.py | 3 +-- .../simcore_service_webserver/users/_users_repository.py | 8 +++++--- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/users.py b/packages/postgres-database/src/simcore_postgres_database/models/users.py index b8ff7a455cdb..7be2161ff864 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/users.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/users.py @@ -95,6 +95,13 @@ # # User Privacy Rules ------------------ # + sa.Column( + "privacy_hide_username", + sa.Boolean, + nullable=False, + server_default=expression.false(), + doc="If true, it hides users.name to others", + ), sa.Column( "privacy_hide_fullname", sa.Boolean, diff --git a/packages/postgres-database/src/simcore_postgres_database/utils_users.py b/packages/postgres-database/src/simcore_postgres_database/utils_users.py index 2a34a71d5832..7aa5c442d379 100644 --- a/packages/postgres-database/src/simcore_postgres_database/utils_users.py +++ b/packages/postgres-database/src/simcore_postgres_database/utils_users.py @@ -242,9 +242,16 @@ def is_public(hide_attribute: Column, caller_id: int): return hide_attribute.is_(False) | (users.c.id == caller_id) -def visible_user_profile_cols(caller_id: int): +def visible_user_profile_cols(caller_id: int, *, username_label: str): """Returns user profile columns with visibility constraints applied based on privacy settings.""" return ( + sa.case( + ( + is_private(users.c.privacy_hide_username, caller_id), + None, + ), + else_=users.c.name, + ).label(username_label), sa.case( ( is_private(users.c.privacy_hide_email, caller_id), diff --git a/services/web/server/src/simcore_service_webserver/groups/_groups_repository.py b/services/web/server/src/simcore_service_webserver/groups/_groups_repository.py index 18d4f01abde0..83740fce3920 100644 --- a/services/web/server/src/simcore_service_webserver/groups/_groups_repository.py +++ b/services/web/server/src/simcore_service_webserver/groups/_groups_repository.py @@ -440,8 +440,7 @@ async def get_user_from_email( def _group_user_cols(caller_id: UserID): return ( users.c.id, - users.c.name, - *visible_user_profile_cols(caller_id), + *visible_user_profile_cols(caller_id, username_label="name"), users.c.primary_gid, ) diff --git a/services/web/server/src/simcore_service_webserver/users/_users_repository.py b/services/web/server/src/simcore_service_webserver/users/_users_repository.py index 49de351c5dcb..6bb1381a92d9 100644 --- a/services/web/server/src/simcore_service_webserver/users/_users_repository.py +++ b/services/web/server/src/simcore_service_webserver/users/_users_repository.py @@ -59,8 +59,7 @@ def _public_user_cols(caller_id: int): return ( # Fits PublicUser model users.c.id.label("user_id"), - users.c.name.label("user_name"), - *visible_user_profile_cols(caller_id), + *visible_user_profile_cols(caller_id, username_label="user_name"), users.c.primary_gid.label("group_id"), ) @@ -103,7 +102,10 @@ async def search_public_user( query = ( sa.select(*_public_user_cols(caller_id=caller_id)) .where( - users.c.name.ilike(_pattern) + ( + is_public(users.c.privacy_hide_username, caller_id) + & users.c.name.ilike(_pattern) + ) | ( is_public(users.c.privacy_hide_email, caller_id) & users.c.email.ilike(_pattern) From 47d8bc5e48e8d6f76a37c52d6adfa376d948d3d4 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:14:09 +0100 Subject: [PATCH 03/50] migration --- ..._new_users_privacy_hide_username_column.py | 36 +++++++++++++++++++ .../users/_users_repository.py | 7 ++-- .../users/_users_service.py | 2 +- 3 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 packages/postgres-database/src/simcore_postgres_database/migration/versions/8403acca8759_new_users_privacy_hide_username_column.py diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/8403acca8759_new_users_privacy_hide_username_column.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/8403acca8759_new_users_privacy_hide_username_column.py new file mode 100644 index 000000000000..91e5a72207fe --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/8403acca8759_new_users_privacy_hide_username_column.py @@ -0,0 +1,36 @@ +"""new users.privacy_hide_username column + +Revision ID: 8403acca8759 +Revises: f7f3c835f38a +Create Date: 2025-03-20 14:08:48.321587+00:00 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "8403acca8759" +down_revision = "f7f3c835f38a" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "users", + sa.Column( + "privacy_hide_username", + sa.Boolean(), + server_default=sa.text("false"), + nullable=False, + ), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("users", "privacy_hide_username") + # ### end Alembic commands ### diff --git a/services/web/server/src/simcore_service_webserver/users/_users_repository.py b/services/web/server/src/simcore_service_webserver/users/_users_repository.py index 6bb1381a92d9..c7a179af2409 100644 --- a/services/web/server/src/simcore_service_webserver/users/_users_repository.py +++ b/services/web/server/src/simcore_service_webserver/users/_users_repository.py @@ -154,7 +154,10 @@ async def get_user_or_raise( async def get_user_primary_group_id( - engine: AsyncEngine, connection: AsyncConnection | None = None, *, user_id: UserID + engine: AsyncEngine, + connection: AsyncConnection | None = None, + *, + user_id: UserID, ) -> GroupID: async with pass_or_acquire_connection(engine, connection) as conn: primary_gid: GroupID | None = await conn.scalar( @@ -182,7 +185,7 @@ async def get_users_ids_in_group( return {row.uid async for row in result} -async def get_user_id_from_pgid(app: web.Application, primary_gid: int) -> UserID: +async def get_user_id_from_pgid(app: web.Application, *, primary_gid: int) -> UserID: async with pass_or_acquire_connection(engine=get_asyncpg_engine(app)) as conn: user_id: UserID = await conn.scalar( sa.select( diff --git a/services/web/server/src/simcore_service_webserver/users/_users_service.py b/services/web/server/src/simcore_service_webserver/users/_users_service.py index bbc5ad6e54d4..5d71423646da 100644 --- a/services/web/server/src/simcore_service_webserver/users/_users_service.py +++ b/services/web/server/src/simcore_service_webserver/users/_users_service.py @@ -122,7 +122,7 @@ async def get_user_primary_group_id(app: web.Application, user_id: UserID) -> Gr async def get_user_id_from_gid(app: web.Application, primary_gid: GroupID) -> UserID: - return await _users_repository.get_user_id_from_pgid(app, primary_gid) + return await _users_repository.get_user_id_from_pgid(app, primary_gid=primary_gid) async def search_users( From c7b2357aab64c0f89098660ed81a94e51664d638 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:15:01 +0100 Subject: [PATCH 04/50] update OAS --- .../src/simcore_service_webserver/api/v0/openapi.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 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 79d94f09c73b..9fc6cf327821 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 @@ -15579,9 +15579,11 @@ components: title: Groupid minimum: 0 userName: - type: string - maxLength: 100 - minLength: 1 + anyOf: + - type: string + maxLength: 100 + minLength: 1 + - type: 'null' title: Username firstName: anyOf: @@ -15603,7 +15605,6 @@ components: required: - userId - groupId - - userName title: UserGet UserNotification: properties: From badef8f1691864f63e7de2401a3ba64631fb51fb Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:15:07 +0100 Subject: [PATCH 05/50] =?UTF-8?q?services/webserver=20api=20version:=200.6?= =?UTF-8?q?1.2=20=E2=86=92=200.61.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/web/server/VERSION | 2 +- services/web/server/setup.cfg | 2 +- .../server/src/simcore_service_webserver/api/v0/openapi.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/web/server/VERSION b/services/web/server/VERSION index 905fae0d2773..d1952dc561e0 100644 --- a/services/web/server/VERSION +++ b/services/web/server/VERSION @@ -1 +1 @@ -0.61.2 +0.61.3 diff --git a/services/web/server/setup.cfg b/services/web/server/setup.cfg index 0cfaac05039e..9d731c416393 100644 --- a/services/web/server/setup.cfg +++ b/services/web/server/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.61.2 +current_version = 0.61.3 commit = True message = services/webserver api version: {current_version} → {new_version} tag = False 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 9fc6cf327821..6804dce5e133 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 @@ -2,7 +2,7 @@ openapi: 3.1.0 info: title: simcore-service-webserver description: Main service with an interface (http-API & websockets) to the web front-end - version: 0.61.2 + version: 0.61.3 servers: - url: '' description: webserver From 8e89260ca0090a1d7381d44e5e777b5ba804a5b8 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:46:22 +0100 Subject: [PATCH 06/50] fixes tests --- .../src/models_library/groups.py | 5 ++-- .../tests/unit/with_dbs/03/test_users.py | 29 ++++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/models-library/src/models_library/groups.py b/packages/models-library/src/models_library/groups.py index c0d8692b2e7e..d35b1de7dcca 100644 --- a/packages/models-library/src/models_library/groups.py +++ b/packages/models-library/src/models_library/groups.py @@ -9,8 +9,7 @@ TypedDict, ) -from .basic_types import IDStr -from .users import UserID +from .users import UserID, UserNameID from .utils.common_validators import create_enums_pre_validator EVERYONE_GROUP_ID: Final[int] = 1 @@ -99,10 +98,10 @@ class GroupsByTypeTuple(NamedTuple): class GroupMember(BaseModel): # identifiers id: UserID - name: IDStr primary_gid: GroupID # private profile + name: UserNameID | None email: EmailStr | None first_name: str | None last_name: str | None diff --git a/services/web/server/tests/unit/with_dbs/03/test_users.py b/services/web/server/tests/unit/with_dbs/03/test_users.py index f5b453bbe254..cefcfa8b8c76 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_users.py +++ b/services/web/server/tests/unit/with_dbs/03/test_users.py @@ -63,7 +63,7 @@ def app_environment( @pytest.fixture def partial_first_name() -> str: - return "James" + return "Jaimito" @pytest.fixture @@ -145,9 +145,9 @@ async def test_search_users_by_partial_fullname( logged_user: UserInfoDict, client: TestClient, partial_first_name: str, - public_user: UserInfoDict, - semi_private_user: UserInfoDict, private_user: UserInfoDict, + semi_private_user: UserInfoDict, + public_user: UserInfoDict, ): assert client.app assert user_role.value == logged_user["role"] @@ -177,9 +177,9 @@ async def test_search_users_by_partial_fullname( @pytest.mark.parametrize("user_role", [UserRole.USER]) async def test_search_users_by_partial_email( + user_role: UserRole, logged_user: UserInfoDict, client: TestClient, - user_role: UserRole, partial_email: str, public_user: UserInfoDict, semi_private_user: UserInfoDict, @@ -216,11 +216,12 @@ async def test_search_users_by_partial_email( await assert_status(resp, status.HTTP_403_FORBIDDEN) +@pytest.mark.parametrize("user_role", [UserRole.USER]) async def test_search_users_by_partial_username( + user_role: UserRole, logged_user: UserInfoDict, client: TestClient, partial_username: str, - user_role: UserRole, public_user: UserInfoDict, semi_private_user: UserInfoDict, private_user: UserInfoDict, @@ -249,8 +250,8 @@ async def test_search_users_by_partial_username( index = (index + 1) % 2 assert found[index].user_name == semi_private_user["name"] assert found[index].email is None - assert found[index].first_name is None - assert found[index].last_name is None + assert found[index].first_name == semi_private_user.get("first_name") + assert found[index].last_name == semi_private_user.get("last_name") @pytest.mark.acceptance_test( @@ -258,9 +259,9 @@ async def test_search_users_by_partial_username( ) @pytest.mark.parametrize("user_role", [UserRole.USER]) async def test_get_user_by_group_id( + user_role: UserRole, logged_user: UserInfoDict, client: TestClient, - user_role: UserRole, public_user: UserInfoDict, private_user: UserInfoDict, ): @@ -329,9 +330,9 @@ async def test_access_rights_on_get_profile( ], ) async def test_access_update_profile( + user_role: UserRole, logged_user: UserInfoDict, client: TestClient, - user_role: UserRole, expected: HTTPStatus, ): assert client.app @@ -345,9 +346,9 @@ async def test_access_update_profile( @pytest.mark.parametrize("user_role", [UserRole.USER]) async def test_get_profile( + user_role: UserRole, logged_user: UserInfoDict, client: TestClient, - user_role: UserRole, primary_group: dict[str, Any], standard_groups: list[dict[str, Any]], all_group: dict[str, str], @@ -393,9 +394,9 @@ async def test_get_profile( @pytest.mark.parametrize("user_role", [UserRole.USER]) async def test_update_profile( + user_role: UserRole, logged_user: UserInfoDict, client: TestClient, - user_role: UserRole, ): assert client.app @@ -434,9 +435,9 @@ def _copy(data: dict, exclude: set) -> dict: @pytest.mark.parametrize("user_role", [UserRole.USER]) async def test_profile_workflow( + user_role: UserRole, logged_user: UserInfoDict, client: TestClient, - user_role: UserRole, ): assert client.app @@ -476,9 +477,9 @@ async def test_profile_workflow( @pytest.mark.parametrize("user_role", [UserRole.USER]) @pytest.mark.parametrize("invalid_username", ["", "_foo", "superadmin", "foo..-123"]) async def test_update_wrong_user_name( + user_role: UserRole, logged_user: UserInfoDict, client: TestClient, - user_role: UserRole, invalid_username: str, ): assert client.app @@ -495,10 +496,10 @@ async def test_update_wrong_user_name( @pytest.mark.parametrize("user_role", [UserRole.USER]) async def test_update_existing_user_name( + user_role: UserRole, user: UserInfoDict, logged_user: UserInfoDict, client: TestClient, - user_role: UserRole, ): assert client.app From afe375c9f8190d5431a54d9b157c603dd4a9b05e Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:50:07 +0100 Subject: [PATCH 07/50] cleanuppre --- packages/models-library/src/models_library/users.py | 9 +++++++-- .../simcore_service_webserver/users/_users_repository.py | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/models-library/src/models_library/users.py b/packages/models-library/src/models_library/users.py index c8860171b644..3b8d4344b4b6 100644 --- a/packages/models-library/src/models_library/users.py +++ b/packages/models-library/src/models_library/users.py @@ -25,13 +25,14 @@ class PrivacyDict(TypedDict): + hide_username: bool hide_fullname: bool hide_email: bool class MyProfile(BaseModel): id: UserID - user_name: IDStr + user_name: UserNameID first_name: str | None last_name: str | None email: LowerCaseEmailStr @@ -50,7 +51,11 @@ def _update_json_schema_extra(schema: JsonDict) -> None: "first_name": "PtN5Ab0uv", "last_name": "", "role": "GUEST", - "privacy": {"hide_email": True, "hide_fullname": False}, + "privacy": { + "hide_email": True, + "hide_fullname": False, + "hide_username": False, + }, } } ) diff --git a/services/web/server/src/simcore_service_webserver/users/_users_repository.py b/services/web/server/src/simcore_service_webserver/users/_users_repository.py index c7a179af2409..369fb50c4dbd 100644 --- a/services/web/server/src/simcore_service_webserver/users/_users_repository.py +++ b/services/web/server/src/simcore_service_webserver/users/_users_repository.py @@ -512,6 +512,8 @@ async def get_my_profile(app: web.Application, *, user_id: UserID) -> MyProfile: users.c.email, users.c.role, sa.func.json_build_object( + "hide_username", + users.c.privacy_hide_username, "hide_fullname", users.c.privacy_hide_fullname, "hide_email", From c672b55615612d9a1bccd74cb620d7a0068261de Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:55:06 +0100 Subject: [PATCH 08/50] fixes test --- .../api_schemas_webserver/groups.py | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/groups.py b/packages/models-library/src/models_library/api_schemas_webserver/groups.py index 4755e9c90af9..643c66b817a7 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/groups.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/groups.py @@ -244,33 +244,31 @@ def from_domain_model( GroupGet.from_domain_model(*gi) for gi in groups_by_type.standard ], all=GroupGet.from_domain_model(*groups_by_type.everyone), - product=GroupGet.from_domain_model(*my_product_group) - if my_product_group - else None, + product=( + GroupGet.from_domain_model(*my_product_group) + if my_product_group + else None + ), ) class GroupUserGet(OutputSchemaWithoutCamelCase): - # Identifiers id: Annotated[UserID | None, Field(description="the user's id")] = None - user_name: Annotated[UserNameID, Field(alias="userName")] + user_name: Annotated[ + UserNameID | None, Field(alias="userName", description="None if private") + ] = None gid: Annotated[ GroupID | None, Field(description="the user primary gid"), ] = None - # Private Profile login: Annotated[ LowerCaseEmailStr | None, - Field(description="the user's email, if privacy settings allows"), - ] = None - first_name: Annotated[ - str | None, Field(description="If privacy settings allows") - ] = None - last_name: Annotated[ - str | None, Field(description="If privacy settings allows") + Field(description="the user's email or None if private"), ] = None + first_name: Annotated[str | None, Field(description="None if private")] = None + last_name: Annotated[str | None, Field(description="None if private")] = None gravatar_id: Annotated[ str | None, Field(description="the user gravatar id hash", deprecated=True) ] = None @@ -309,6 +307,11 @@ class GroupUserGet(OutputSchemaWithoutCamelCase): "userName": "mrprivate", "gid": "55", }, + # very private user + { + "id": "6", + "gid": "55", + }, { "id": "56", "userName": "mrpublic", From e72b5d52c228b245c3e75304c3dab2f3c73f8aa9 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:57:31 +0100 Subject: [PATCH 09/50] fixes test --- .../api/v0/openapi.yaml | 17 +++++++++-------- .../server/tests/unit/with_dbs/03/test_users.py | 9 +++++---- 2 files changed, 14 insertions(+), 12 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 6804dce5e133..b6e931af5338 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 @@ -11001,10 +11001,13 @@ components: title: Id description: the user's id userName: - type: string - maxLength: 100 - minLength: 1 + anyOf: + - type: string + maxLength: 100 + minLength: 1 + - type: 'null' title: Username + description: None if private gid: anyOf: - type: integer @@ -11019,19 +11022,19 @@ components: format: email - type: 'null' title: Login - description: the user's email, if privacy settings allows + description: the user's email or None if private first_name: anyOf: - type: string - type: 'null' title: First Name - description: If privacy settings allows + description: None if private last_name: anyOf: - type: string - type: 'null' title: Last Name - description: If privacy settings allows + description: None if private gravatar_id: anyOf: - type: string @@ -11046,8 +11049,6 @@ components: description: If group is standard, these are these are the access rights of the user to it.None if primary group. type: object - required: - - userName title: GroupUserGet example: accessRights: diff --git a/services/web/server/tests/unit/with_dbs/03/test_users.py b/services/web/server/tests/unit/with_dbs/03/test_users.py index cefcfa8b8c76..7e68ba4edd8c 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_users.py +++ b/services/web/server/tests/unit/with_dbs/03/test_users.py @@ -271,7 +271,7 @@ async def test_get_user_by_group_id( assert private_user["id"] != logged_user["id"] assert public_user["id"] != logged_user["id"] - # GET user by primary GID + # GET public_user by its primary gid url = client.app.router["get_all_group_users"].url_for( gid=f"{public_user['primary_gid']}" ) @@ -285,6 +285,7 @@ async def test_get_user_by_group_id( assert users[0].first_name == public_user.get("first_name") assert users[0].last_name == public_user.get("last_name") + # GET private_user by its primary gid url = client.app.router["get_all_group_users"].url_for( gid=f"{private_user['primary_gid']}" ) @@ -294,9 +295,9 @@ async def test_get_user_by_group_id( users = TypeAdapter(list[GroupUserGet]).validate_python(data) assert len(users) == 1 assert users[0].id == private_user["id"] - assert users[0].user_name == private_user["name"] - assert users[0].first_name is None - assert users[0].last_name is None + assert users[0].user_name is None, "It's private" + assert users[0].first_name is None, "It's private" + assert users[0].last_name is None, "It's private" @pytest.mark.parametrize( From d2fbd0407e0a8b53ef9262845242920fb888974f Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:12:54 +0100 Subject: [PATCH 10/50] helpers --- .../pytest_simcore/helpers/webserver_login.py | 26 ++++++++++++ .../with_dbs/03/trash/test_trash_service.py | 41 ++++--------------- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/packages/pytest-simcore/src/pytest_simcore/helpers/webserver_login.py b/packages/pytest-simcore/src/pytest_simcore/helpers/webserver_login.py index ccb0f9587fb2..a9d7b3fcdd74 100644 --- a/packages/pytest-simcore/src/pytest_simcore/helpers/webserver_login.py +++ b/packages/pytest-simcore/src/pytest_simcore/helpers/webserver_login.py @@ -1,4 +1,6 @@ +import contextlib import re +from collections.abc import AsyncIterator from datetime import datetime from typing import Any, TypedDict @@ -186,6 +188,30 @@ async def __aexit__(self, *args): return await super().__aexit__(*args) +@contextlib.asynccontextmanager +async def switch_client_session_to( + client: TestClient, user: UserInfoDict +) -> AsyncIterator[TestClient]: + assert client.app + + await client.post(f'{client.app.router["auth_logout"].url_for()}') + # sometimes 4xx if user already logged out. Ignore + + resp = await client.post( + f'{client.app.router["auth_login"].url_for()}', + json={ + "email": user["email"], + "password": user["raw_password"], + }, + ) + await assert_status(resp, status.HTTP_200_OK) + + yield client + + resp = await client.post(f'{client.app.router["auth_logout"].url_for()}') + await assert_status(resp, status.HTTP_200_OK) + + class NewInvitation(NewUser): def __init__( self, diff --git a/services/web/server/tests/unit/with_dbs/03/trash/test_trash_service.py b/services/web/server/tests/unit/with_dbs/03/trash/test_trash_service.py index 94d67e5ec567..a58f32f6e3d6 100644 --- a/services/web/server/tests/unit/with_dbs/03/trash/test_trash_service.py +++ b/services/web/server/tests/unit/with_dbs/03/trash/test_trash_service.py @@ -6,8 +6,6 @@ # pylint: disable=unused-variable -import contextlib -from collections.abc import AsyncIterator from unittest.mock import MagicMock import pytest @@ -16,7 +14,10 @@ from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict from pytest_simcore.helpers.typing_env import EnvVarsDict -from pytest_simcore.helpers.webserver_login import UserInfoDict +from pytest_simcore.helpers.webserver_login import ( + UserInfoDict, + switch_client_session_to, +) from servicelib.aiohttp import status from simcore_service_webserver.db.models import UserRole from simcore_service_webserver.projects import _trash_service @@ -44,30 +45,6 @@ def user_role() -> UserRole: return UserRole.USER -@contextlib.asynccontextmanager -async def _switch_client_session_to( - client: TestClient, user: UserInfoDict -) -> AsyncIterator[TestClient]: - assert client.app - - await client.post(f'{client.app.router["auth_logout"].url_for()}') - # sometimes 4xx if user already logged out. Ignore - - resp = await client.post( - f'{client.app.router["auth_login"].url_for()}', - json={ - "email": user["email"], - "password": user["raw_password"], - }, - ) - await assert_status(resp, status.HTTP_200_OK) - - yield client - - resp = await client.post(f'{client.app.router["auth_logout"].url_for()}') - await assert_status(resp, status.HTTP_200_OK) - - async def test_trash_service__delete_expired_trash( client: TestClient, logged_user: UserInfoDict, @@ -116,7 +93,7 @@ async def test_trash_service__delete_expired_trash( await assert_status(resp, status.HTTP_404_NOT_FOUND) # ASSERT: other_user tries to get the project and expects 404 - async with _switch_client_session_to(client, other_user): + async with switch_client_session_to(client, other_user): resp = await client.get(f"/v0/projects/{other_user_project_id}") await assert_status(resp, status.HTTP_404_NOT_FOUND) @@ -134,7 +111,7 @@ async def test_trash_nested_folders_and_projects( assert client.app assert logged_user["id"] != other_user["id"] - async with _switch_client_session_to(client, logged_user): + async with switch_client_session_to(client, logged_user): # CREATE folders hierarchy for logged_user resp = await client.post("/v0/folders", json={"name": "Root Folder"}) data, _ = await assert_status(resp, status.HTTP_201_CREATED) @@ -162,7 +139,7 @@ async def test_trash_nested_folders_and_projects( ) await assert_status(resp, status.HTTP_204_NO_CONTENT) - async with _switch_client_session_to(client, other_user): + async with switch_client_session_to(client, other_user): # CREATE folders hierarchy for other_user resp = await client.post("/v0/folders", json={"name": "Root Folder"}) data, _ = await assert_status(resp, status.HTTP_201_CREATED) @@ -193,7 +170,7 @@ async def test_trash_nested_folders_and_projects( # UNDER TEST await trash_service.safe_delete_expired_trash_as_admin(client.app) - async with _switch_client_session_to(client, logged_user): + async with switch_client_session_to(client, logged_user): # Verify logged_user's resources are gone resp = await client.get(f"/v0/folders/{logged_user_root_folder['folderId']}") await assert_status(resp, status.HTTP_403_FORBIDDEN) @@ -205,7 +182,7 @@ async def test_trash_nested_folders_and_projects( await assert_status(resp, status.HTTP_404_NOT_FOUND) # Verify other_user's resources are gone - async with _switch_client_session_to(client, other_user): + async with switch_client_session_to(client, other_user): resp = await client.get(f"/v0/folders/{other_user_root_folder['folderId']}") await assert_status(resp, status.HTTP_403_FORBIDDEN) From ef5f72a423891acc4410b46b704de99f0a50d442 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:25:36 +0100 Subject: [PATCH 11/50] self test --- .../users/_users_repository.py | 2 -- .../tests/unit/with_dbs/03/test_users.py | 34 ++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/users/_users_repository.py b/services/web/server/src/simcore_service_webserver/users/_users_repository.py index 369fb50c4dbd..c7a179af2409 100644 --- a/services/web/server/src/simcore_service_webserver/users/_users_repository.py +++ b/services/web/server/src/simcore_service_webserver/users/_users_repository.py @@ -512,8 +512,6 @@ async def get_my_profile(app: web.Application, *, user_id: UserID) -> MyProfile: users.c.email, users.c.role, sa.func.json_build_object( - "hide_username", - users.c.privacy_hide_username, "hide_fullname", users.c.privacy_hide_fullname, "hide_email", diff --git a/services/web/server/tests/unit/with_dbs/03/test_users.py b/services/web/server/tests/unit/with_dbs/03/test_users.py index 7e68ba4edd8c..2d816aa67b1c 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_users.py +++ b/services/web/server/tests/unit/with_dbs/03/test_users.py @@ -35,7 +35,11 @@ random_pre_registration_details, ) from pytest_simcore.helpers.monkeypatch_envs import EnvVarsDict, setenvs_from_dict -from pytest_simcore.helpers.webserver_login import NewUser, UserInfoDict +from pytest_simcore.helpers.webserver_login import ( + NewUser, + UserInfoDict, + switch_client_session_to, +) from servicelib.aiohttp import status from servicelib.rest_constants import RESPONSE_MODEL_POLICY from simcore_service_webserver.users._common.schemas import ( @@ -226,6 +230,8 @@ async def test_search_users_by_partial_username( semi_private_user: UserInfoDict, private_user: UserInfoDict, ): + assert client.app + # SEARCH by partial username assert partial_username in private_user["name"] assert partial_username in semi_private_user["name"] @@ -254,6 +260,32 @@ async def test_search_users_by_partial_username( assert found[index].last_name == semi_private_user.get("last_name") +async def test_search_myself( + client: TestClient, + public_user: UserInfoDict, + semi_private_user: UserInfoDict, + private_user: UserInfoDict, +): + assert client.app + for user in [public_user, semi_private_user, private_user]: + async with switch_client_session_to(client, user): + + # search me + url = client.app.router["search_users"].url_for() + resp = await client.post(f"{url}", json={"match": user["name"]}) + data, _ = await assert_status(resp, status.HTTP_200_OK) + + found = TypeAdapter(list[UserGet]).validate_python(data) + assert found + assert len(found) == 1 + + # I can see my own data + assert found[0].user_name == user["name"] + assert found[0].email == user["email"] + assert found[0].first_name == user.get("first_name") + assert found[0].last_name == user.get("last_name") + + @pytest.mark.acceptance_test( "https://github.com/ITISFoundation/osparc-issues/issues/1779" ) From 8eb16122ddd0777d6123c8e261b3ef9b78261ff0 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:30:54 +0100 Subject: [PATCH 12/50] fixes test --- .../users/_users_repository.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/users/_users_repository.py b/services/web/server/src/simcore_service_webserver/users/_users_repository.py index c7a179af2409..8f13169e147d 100644 --- a/services/web/server/src/simcore_service_webserver/users/_users_repository.py +++ b/services/web/server/src/simcore_service_webserver/users/_users_repository.py @@ -392,13 +392,9 @@ async def get_user_products( .where(products.c.group_id == groups.c.gid) .label("product_name") ) - products_gis_subq = ( - sa.select( - products.c.group_id, - ) - .distinct() - .subquery() - ) + products_group_ids_subq = sa.select( + products.c.group_id, + ).distinct() query = ( sa.select( groups.c.gid, @@ -408,7 +404,7 @@ async def get_user_products( users.join(user_to_groups, user_to_groups.c.uid == users.c.id).join( groups, (groups.c.gid == user_to_groups.c.gid) - & groups.c.gid.in_(products_gis_subq), + & groups.c.gid.in_(products_group_ids_subq), ) ) .where(users.c.id == user_id) @@ -512,6 +508,8 @@ async def get_my_profile(app: web.Application, *, user_id: UserID) -> MyProfile: users.c.email, users.c.role, sa.func.json_build_object( + "hide_username", + users.c.privacy_hide_username, "hide_fullname", users.c.privacy_hide_fullname, "hide_email", From 3431e8da9bf3aeafa21880dd6d5c0255f8baf6d5 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Fri, 21 Mar 2025 13:16:26 +0100 Subject: [PATCH 13/50] hideUsername field and message --- .../osparc/desktop/account/ProfilePage.js | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js index f74cb9aed5aa..c22c8f2ba10b 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js +++ b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js @@ -75,6 +75,7 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { if (privacyData) { this.__userPrivacyData = privacyData; this.__userPrivacyModel.set({ + "hideUsername": "hideUsername" in privacyData ? privacyData["hideUsername"] : false, "hideFullname": "hideFullname" in privacyData ? privacyData["hideFullname"] : true, "hideEmail": "hideEmail" in privacyData ? privacyData["hideEmail"] : true, }); @@ -224,6 +225,9 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { const label = osparc.ui.window.TabbedView.createHelpLabel(this.tr("For Privacy reasons, you might want to hide your First and Last Names and/or the Email to other users")); box.add(label); + const hideUsername = new qx.ui.form.CheckBox().set({ + value: false + }); const hideFullname = new qx.ui.form.CheckBox().set({ value: true }); @@ -232,18 +236,21 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { }); const form = new qx.ui.form.Form(); + form.add(hideUsername, "Hide Username", null, "hideUsername"); form.add(hideFullname, "Hide Full Name", null, "hideFullname"); form.add(hideEmail, "Hide Email", null, "hideEmail"); box.add(new qx.ui.form.renderer.Single(form)); // binding to a model const raw = { + "hideUsername": false, "hideFullname": true, "hideEmail": true, }; const model = this.__userPrivacyModel = qx.data.marshal.Json.createModel(raw); const controller = new qx.data.controller.Object(model); + controller.addTarget(hideUsername, "value", "hideUsername", true); controller.addTarget(hideFullname, "value", "hideFullname", true); controller.addTarget(hideEmail, "value", "hideEmail", true); @@ -261,6 +268,9 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { const patchData = { "privacy": {} }; + if (this.__userPrivacyData["hideUsername"] !== model.getHideUsername()) { + patchData["privacy"]["hideUsername"] = model.getHideUsername(); + } if (this.__userPrivacyData["hideFullname"] !== model.getHideFullname()) { patchData["privacy"]["hideFullname"] = model.getHideFullname(); } @@ -296,6 +306,25 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { osparc.FlashMessenger.logError(err, this.tr("Unsuccessful privacy update")); }); } + + const optOutMessage = new qx.ui.basic.Atom().set({ + label: "If all searchable fields are showHidden, you will not be ", + icon: "@FontAwesome5Solid/copy/10", + iconPosition: "right", + gap: 8, + cursor: "pointer", + alignX: "center", + allowGrowX: false, + visibility: "excluded", + }); + box.add(optOutMessage); + if ( + this.__userPrivacyModel.getHideUsername() && + this.__userPrivacyModel.getHideFullname() && + this.__userPrivacyModel.getHideEmail() + ) { + optOutMessage.show(); + } }); return box; From 2057d344c55b3b616c62da9d9c93e12e37498747 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Fri, 21 Mar 2025 13:32:30 +0100 Subject: [PATCH 14/50] optOutMessage --- .../osparc/desktop/account/ProfilePage.js | 71 ++++++++++--------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js index c22c8f2ba10b..2adf6faa1f07 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js +++ b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js @@ -216,15 +216,43 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { }, __createPrivacySection: function() { + // binding to a model + const raw = { + "hideUsername": false, + "hideFullname": true, + "hideEmail": true, + }; + + const privacyModel = this.__userPrivacyModel = qx.data.marshal.Json.createModel(raw); + const box = osparc.ui.window.TabbedView.createSectionBox(this.tr("Privacy")); box.set({ alignX: "left", maxWidth: 500 }); - const label = osparc.ui.window.TabbedView.createHelpLabel(this.tr("For Privacy reasons, you might want to hide your First and Last Names and/or the Email to other users")); + const label = osparc.ui.window.TabbedView.createHelpLabel(this.tr("For Privacy reasons, you might want to hide some personal fields.")); box.add(label); + const optOutMessage = new qx.ui.basic.Atom().set({ + label: "If all searchable fields are showHidden, you will not be ", + icon: "@FontAwesome5Solid/copy/10", + iconPosition: "right", + gap: 8, + cursor: "pointer", + alignX: "center", + allowGrowX: false, + visibility: "excluded", + }); + box.add(optOutMessage); + if ( + this.__userPrivacyModel.getHideUsername() && + this.__userPrivacyModel.getHideFullname() && + this.__userPrivacyModel.getHideEmail() + ) { + optOutMessage.show(); + } + const hideUsername = new qx.ui.form.CheckBox().set({ value: false }); @@ -241,15 +269,7 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { form.add(hideEmail, "Hide Email", null, "hideEmail"); box.add(new qx.ui.form.renderer.Single(form)); - // binding to a model - const raw = { - "hideUsername": false, - "hideFullname": true, - "hideEmail": true, - }; - - const model = this.__userPrivacyModel = qx.data.marshal.Json.createModel(raw); - const controller = new qx.data.controller.Object(model); + const controller = new qx.data.controller.Object(privacyModel); controller.addTarget(hideUsername, "value", "hideUsername", true); controller.addTarget(hideFullname, "value", "hideFullname", true); controller.addTarget(hideEmail, "value", "hideEmail", true); @@ -268,14 +288,14 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { const patchData = { "privacy": {} }; - if (this.__userPrivacyData["hideUsername"] !== model.getHideUsername()) { - patchData["privacy"]["hideUsername"] = model.getHideUsername(); + if (this.__userPrivacyData["hideUsername"] !== privacyModel.getHideUsername()) { + patchData["privacy"]["hideUsername"] = privacyModel.getHideUsername(); } - if (this.__userPrivacyData["hideFullname"] !== model.getHideFullname()) { - patchData["privacy"]["hideFullname"] = model.getHideFullname(); + if (this.__userPrivacyData["hideFullname"] !== privacyModel.getHideFullname()) { + patchData["privacy"]["hideFullname"] = privacyModel.getHideFullname(); } - if (this.__userPrivacyData["hideEmail"] !== model.getHideEmail()) { - patchData["privacy"]["hideEmail"] = model.getHideEmail(); + if (this.__userPrivacyData["hideEmail"] !== privacyModel.getHideEmail()) { + patchData["privacy"]["hideEmail"] = privacyModel.getHideEmail(); } if ( @@ -306,25 +326,6 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { osparc.FlashMessenger.logError(err, this.tr("Unsuccessful privacy update")); }); } - - const optOutMessage = new qx.ui.basic.Atom().set({ - label: "If all searchable fields are showHidden, you will not be ", - icon: "@FontAwesome5Solid/copy/10", - iconPosition: "right", - gap: 8, - cursor: "pointer", - alignX: "center", - allowGrowX: false, - visibility: "excluded", - }); - box.add(optOutMessage); - if ( - this.__userPrivacyModel.getHideUsername() && - this.__userPrivacyModel.getHideFullname() && - this.__userPrivacyModel.getHideEmail() - ) { - optOutMessage.show(); - } }); return box; From 9cd1a0cddf50211d9aa473df713defc4071c000c Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Fri, 21 Mar 2025 13:45:38 +0100 Subject: [PATCH 15/50] optOutMessage --- .../osparc/desktop/account/ProfilePage.js | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js index 2adf6faa1f07..2b4c85e1973c 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js +++ b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js @@ -231,28 +231,9 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { maxWidth: 500 }); - const label = osparc.ui.window.TabbedView.createHelpLabel(this.tr("For Privacy reasons, you might want to hide some personal fields.")); + const label = osparc.ui.window.TabbedView.createHelpLabel(this.tr("For Privacy reasons, you might want to hide some personal data.")); box.add(label); - const optOutMessage = new qx.ui.basic.Atom().set({ - label: "If all searchable fields are showHidden, you will not be ", - icon: "@FontAwesome5Solid/copy/10", - iconPosition: "right", - gap: 8, - cursor: "pointer", - alignX: "center", - allowGrowX: false, - visibility: "excluded", - }); - box.add(optOutMessage); - if ( - this.__userPrivacyModel.getHideUsername() && - this.__userPrivacyModel.getHideFullname() && - this.__userPrivacyModel.getHideEmail() - ) { - optOutMessage.show(); - } - const hideUsername = new qx.ui.form.CheckBox().set({ value: false }); @@ -328,6 +309,29 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { } }); + const optOutMessage = new qx.ui.basic.Atom().set({ + label: "If all searchable fields are hidden, you will not be findable.", + icon: "@FontAwesome5Solid/exclamation-triangle/14", + gap: 8, + allowGrowX: false, + }); + optOutMessage.getChildControl("icon").setTextColor("warning-yellow") + box.add(optOutMessage); + const privacyFields = [ + hideUsername, + hideFullname, + hideEmail, + ] + const evaluateWarningMessage = () => { + if (privacyFields.every(privacyField => privacyField.getValue())) { + optOutMessage.show(); + } else { + optOutMessage.exclude(); + } + }; + evaluateWarningMessage(); + privacyFields.forEach(privacyField => privacyField.addListener("changeValue", () => evaluateWarningMessage())); + return box; }, From 43c5e89dac0c1498a3f5a37b76036a463c7c0c30 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Fri, 21 Mar 2025 14:05:52 +0100 Subject: [PATCH 16/50] eyes next to the fields --- .../osparc/desktop/account/ProfilePage.js | 9 ++- .../osparc/ui/form/renderer/SingleWithIcon.js | 62 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 services/static-webserver/client/source/class/osparc/ui/form/renderer/SingleWithIcon.js diff --git a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js index 2b4c85e1973c..d7f2455b0e12 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js +++ b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js @@ -111,7 +111,14 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { form.add(firstName, "First Name", null, "firstName"); form.add(lastName, "Last Name", null, "lastName"); form.add(email, "Email", null, "email"); - box.add(new qx.ui.form.renderer.Single(form)); + const icons = { + 0: "@FontAwesome5Solid/eye/12", + 1: "@FontAwesome5Solid/eye-slash/12", + 2: "@FontAwesome5Solid/eye-slash/12", + 3: "@FontAwesome5Solid/eye-slash/12", + }; + const singleWithIcon = new osparc.ui.form.renderer.SingleWithIcon(form, icons); + box.add(singleWithIcon); const expirationLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)).set({ paddingLeft: 16, diff --git a/services/static-webserver/client/source/class/osparc/ui/form/renderer/SingleWithIcon.js b/services/static-webserver/client/source/class/osparc/ui/form/renderer/SingleWithIcon.js new file mode 100644 index 000000000000..58515f8b220f --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/ui/form/renderer/SingleWithIcon.js @@ -0,0 +1,62 @@ +/* ************************************************************************ + + 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.ui.form.renderer.SingleWithIcon", { + extend: qx.ui.form.renderer.Single, + + construct: function(form, icons) { + if (icons) { + this.__icons = icons; + } else { + this.__icons = {}; + } + + this.base(arguments, form); + }, + + members: { + __icons: null, + + setIcons(icons) { + this.__icons = icons; + + this._render(); + }, + + // overridden + addItems: function(items, names, title, itemOptions, headerOptions) { + this.base(arguments, items, names, title, itemOptions, headerOptions); + + // header + let row = title === null ? 0 : 1; + + for (let i = 0; i < items.length; i++) { + if (i in this.__icons) { + const image = new qx.ui.basic.Image(this.__icons[i]).set({ + alignY: "middle", + }); + this._add(image, { + row, + column: 2, + }); + } + + row++; + } + }, + } +}); From 12264dd4d4ad01556b3cb504b441a3e78bde418f Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Fri, 21 Mar 2025 14:15:33 +0100 Subject: [PATCH 17/50] dynamic eyes --- .../osparc/desktop/account/ProfilePage.js | 19 ++++++++++++------- .../osparc/ui/form/renderer/SingleWithIcon.js | 4 ++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js index d7f2455b0e12..c3609025fad7 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js +++ b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js @@ -45,6 +45,7 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { members: { __userProfileData: null, __userProfileModel: null, + __userProfileRenderer: null, __userPrivacyData: null, __userPrivacyModel: null, __userProfileForm: null, @@ -79,6 +80,16 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { "hideFullname": "hideFullname" in privacyData ? privacyData["hideFullname"] : true, "hideEmail": "hideEmail" in privacyData ? privacyData["hideEmail"] : true, }); + + const visibleIcon = "@FontAwesome5Solid/eye/12"; + const hiddenIcon = "@FontAwesome5Solid/eye-slash/12"; + const icons = { + 0: this.__userPrivacyModel.getHideUsername() ? hiddenIcon : visibleIcon, + 1: this.__userPrivacyModel.getHideFullname() ? hiddenIcon : visibleIcon, + 2: this.__userPrivacyModel.getHideFullname() ? hiddenIcon : visibleIcon, + 3: this.__userPrivacyModel.getHideEmail() ? hiddenIcon : visibleIcon, + }; + this.__userProfileRenderer.setIcons(icons); } }, @@ -111,13 +122,7 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { form.add(firstName, "First Name", null, "firstName"); form.add(lastName, "Last Name", null, "lastName"); form.add(email, "Email", null, "email"); - const icons = { - 0: "@FontAwesome5Solid/eye/12", - 1: "@FontAwesome5Solid/eye-slash/12", - 2: "@FontAwesome5Solid/eye-slash/12", - 3: "@FontAwesome5Solid/eye-slash/12", - }; - const singleWithIcon = new osparc.ui.form.renderer.SingleWithIcon(form, icons); + const singleWithIcon = this.__userProfileRenderer = new osparc.ui.form.renderer.SingleWithIcon(form); box.add(singleWithIcon); const expirationLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)).set({ diff --git a/services/static-webserver/client/source/class/osparc/ui/form/renderer/SingleWithIcon.js b/services/static-webserver/client/source/class/osparc/ui/form/renderer/SingleWithIcon.js index 58515f8b220f..a8252d6040c2 100644 --- a/services/static-webserver/client/source/class/osparc/ui/form/renderer/SingleWithIcon.js +++ b/services/static-webserver/client/source/class/osparc/ui/form/renderer/SingleWithIcon.js @@ -31,10 +31,10 @@ qx.Class.define("osparc.ui.form.renderer.SingleWithIcon", { members: { __icons: null, - setIcons(icons) { + setIcons: function(icons) { this.__icons = icons; - this._render(); + this._onFormChange(); }, // overridden From b04891604d2c1d2bcb788a0268d4919448959cd0 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Fri, 21 Mar 2025 14:21:58 +0100 Subject: [PATCH 18/50] minor --- .../client/source/class/osparc/desktop/account/ProfilePage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js index c3609025fad7..d7968b5af83c 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js +++ b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js @@ -322,7 +322,7 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { }); const optOutMessage = new qx.ui.basic.Atom().set({ - label: "If all searchable fields are hidden, you will not be findable.", + label: this.tr("If all searchable fields are hidden, you will not be findable."), icon: "@FontAwesome5Solid/exclamation-triangle/14", gap: 8, allowGrowX: false, From 27c13c89fe6fa12564c25f8d71e0a27ca6150cd3 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Fri, 21 Mar 2025 14:25:02 +0100 Subject: [PATCH 19/50] username is nullable --- .../client/source/class/osparc/data/model/User.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/static-webserver/client/source/class/osparc/data/model/User.js b/services/static-webserver/client/source/class/osparc/data/model/User.js index a8f39a3779f4..8b134c79fc0a 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/User.js +++ b/services/static-webserver/client/source/class/osparc/data/model/User.js @@ -30,7 +30,7 @@ qx.Class.define("osparc.data.model.User", { const userId = ("id" in userData) ? parseInt(userData["id"]) : parseInt(userData["userId"]); const groupId = ("gid" in userData) ? parseInt(userData["gid"]) : parseInt(userData["groupId"]); - const username = userData["userName"]; + const username = userData["userName"] || "-"; const email = ("login" in userData) ? userData["login"] : userData["email"]; let firstName = ""; if (userData["first_name"]) { From c11d3a00c32fa7776fd62a609ff2290d322398fb Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Fri, 21 Mar 2025 14:30:00 +0100 Subject: [PATCH 20/50] minor --- .../client/source/class/osparc/data/model/User.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/static-webserver/client/source/class/osparc/data/model/User.js b/services/static-webserver/client/source/class/osparc/data/model/User.js index 8b134c79fc0a..47d665f847d1 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/User.js +++ b/services/static-webserver/client/source/class/osparc/data/model/User.js @@ -60,7 +60,7 @@ qx.Class.define("osparc.data.model.User", { lastName, email, thumbnail, - label: username, + label: userData["userName"] || description, description, }); }, From f138b4c20130cf88384ad5fe5194ff52b4d37ec2 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:39:30 +0100 Subject: [PATCH 21/50] missing fields --- .../models_library/api_schemas_webserver/users.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 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 947623fc9567..1b0362d82d1f 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 @@ -16,6 +16,7 @@ ValidationInfo, field_validator, ) +from simcore_postgres_database.utils_users import MIN_USERNAME_LEN from ..basic_types import IDStr from ..emails import LowerCaseEmailStr @@ -46,11 +47,13 @@ class MyProfilePrivacyGet(OutputSchema): + hide_username: bool hide_fullname: bool hide_email: bool class MyProfilePrivacyPatch(InputSchema): + hide_username: bool | None = None hide_fullname: bool | None = None hide_email: bool | None = None @@ -92,7 +95,11 @@ class MyProfileGet(OutputSchemaWithoutCamelCase): "role": "admin", # pre "expirationDate": "2022-09-14", # optional "preferences": {}, - "privacy": {"hide_fullname": 0, "hide_email": 1}, + "privacy": { + "hide_username": 0, + "hide_fullname": 0, + "hide_email": 1, + }, }, ] }, @@ -141,7 +148,9 @@ def from_domain_model( class MyProfilePatch(InputSchemaWithoutCamelCase): first_name: FirstNameStr | None = None last_name: LastNameStr | None = None - user_name: Annotated[IDStr | None, Field(alias="userName", min_length=4)] = None + user_name: Annotated[ + IDStr | None, Field(alias="userName", min_length=MIN_USERNAME_LEN) + ] = None privacy: MyProfilePrivacyPatch | None = None From 3a30f396832bdc332169baf593d7568a5982754f Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:39:39 +0100 Subject: [PATCH 22/50] constant --- .../src/simcore_postgres_database/utils_users.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/postgres-database/src/simcore_postgres_database/utils_users.py b/packages/postgres-database/src/simcore_postgres_database/utils_users.py index 7aa5c442d379..c35123c9545b 100644 --- a/packages/postgres-database/src/simcore_postgres_database/utils_users.py +++ b/packages/postgres-database/src/simcore_postgres_database/utils_users.py @@ -27,10 +27,10 @@ class UserNotFoundInRepoError(BaseUserRepoError): # NOTE: see MyProfilePatch.user_name -_MIN_USERNAME_LEN: Final[int] = 4 +MIN_USERNAME_LEN: Final[int] = 4 -def _generate_random_chars(length: int = _MIN_USERNAME_LEN) -> str: +def _generate_random_chars(length: int = MIN_USERNAME_LEN) -> str: """returns `length` random digit character""" return "".join(secrets.choice(string.digits) for _ in range(length)) @@ -42,8 +42,8 @@ def _generate_username_from_email(email: str) -> str: username = re.sub(r"[^a-zA-Z0-9]", "", username).lower() # Ensure the username is at least 4 characters long - if len(username) < _MIN_USERNAME_LEN: - username += _generate_random_chars(length=_MIN_USERNAME_LEN - len(username)) + if len(username) < MIN_USERNAME_LEN: + username += _generate_random_chars(length=MIN_USERNAME_LEN - len(username)) return username From 4aa012ac4abb7828dd4a760caa23024a430d8b6c Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:43:59 +0100 Subject: [PATCH 23/50] refactor --- .../api_schemas_webserver/users.py | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 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 1b0362d82d1f..a2f162602cce 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 @@ -16,6 +16,7 @@ ValidationInfo, field_validator, ) +from pydantic.config import JsonDict from simcore_postgres_database.utils_users import MIN_USERNAME_LEN from ..basic_types import IDStr @@ -82,27 +83,33 @@ class MyProfileGet(OutputSchemaWithoutCamelCase): privacy: MyProfilePrivacyGet preferences: AggregatedPreferences + @staticmethod + def _update_json_schema_extra(schema: JsonDict) -> None: + schema.update( + { + "examples": [ + { + "id": 42, + "login": "bla@foo.com", + "userName": "bla42", + "role": "admin", # pre + "expirationDate": "2022-09-14", # optional + "preferences": {}, + "privacy": { + "hide_username": 0, + "hide_fullname": 0, + "hide_email": 1, + }, + }, + ] + } + ) + model_config = ConfigDict( # NOTE: old models have an hybrid between snake and camel cases! # Should be unified at some point populate_by_name=True, - json_schema_extra={ - "examples": [ - { - "id": 42, - "login": "bla@foo.com", - "userName": "bla42", - "role": "admin", # pre - "expirationDate": "2022-09-14", # optional - "preferences": {}, - "privacy": { - "hide_username": 0, - "hide_fullname": 0, - "hide_email": 1, - }, - }, - ] - }, + json_schema_extra=_update_json_schema_extra, ) @field_validator("role", mode="before") From 86422c2497b02014d2522eb9fecdde9b3bfee1c6 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:50:32 +0100 Subject: [PATCH 24/50] test --- services/web/server/tests/unit/with_dbs/03/test_users.py | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/server/tests/unit/with_dbs/03/test_users.py b/services/web/server/tests/unit/with_dbs/03/test_users.py index 2d816aa67b1c..c4008f752356 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_users.py +++ b/services/web/server/tests/unit/with_dbs/03/test_users.py @@ -503,6 +503,7 @@ async def test_profile_workflow( assert updated_profile.user_name == "odei123" assert updated_profile.privacy != my_profile.privacy + assert updated_profile.privacy.hide_username == my_profile.privacy.hide_username assert updated_profile.privacy.hide_email == my_profile.privacy.hide_email assert updated_profile.privacy.hide_fullname != my_profile.privacy.hide_fullname From 38400c17cad2071919c329c2396ed19298c9c7bf Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:50:46 +0100 Subject: [PATCH 25/50] retire put --- api/specs/web-server/_users.py | 51 +++++-------------- .../users/_users_rest.py | 3 -- 2 files changed, 14 insertions(+), 40 deletions(-) diff --git a/api/specs/web-server/_users.py b/api/specs/web-server/_users.py index 89d5eaaba2f9..d0d733a01e34 100644 --- a/api/specs/web-server/_users.py +++ b/api/specs/web-server/_users.py @@ -39,26 +39,14 @@ "/me", response_model=Envelope[MyProfileGet], ) -async def get_my_profile(): - ... +async def get_my_profile(): ... @router.patch( "/me", status_code=status.HTTP_204_NO_CONTENT, ) -async def update_my_profile(_body: MyProfilePatch): - ... - - -@router.put( - "/me", - status_code=status.HTTP_204_NO_CONTENT, - deprecated=True, - description="Use PATCH instead", -) -async def replace_my_profile(_body: MyProfilePatch): - ... +async def update_my_profile(_body: MyProfilePatch): ... @router.patch( @@ -68,16 +56,14 @@ async def replace_my_profile(_body: MyProfilePatch): async def set_frontend_preference( preference_id: PreferenceIdentifier, _body: PatchRequestBody, -): - ... +): ... @router.get( "/me/tokens", response_model=Envelope[list[MyTokenGet]], ) -async def list_tokens(): - ... +async def list_tokens(): ... @router.post( @@ -85,8 +71,7 @@ async def list_tokens(): response_model=Envelope[MyTokenGet], status_code=status.HTTP_201_CREATED, ) -async def create_token(_body: MyTokenCreate): - ... +async def create_token(_body: MyTokenCreate): ... @router.get( @@ -95,24 +80,21 @@ async def create_token(_body: MyTokenCreate): ) async def get_token( _path: Annotated[_TokenPathParams, Depends()], -): - ... +): ... @router.delete( "/me/tokens/{service}", status_code=status.HTTP_204_NO_CONTENT, ) -async def delete_token(_path: Annotated[_TokenPathParams, Depends()]): - ... +async def delete_token(_path: Annotated[_TokenPathParams, Depends()]): ... @router.get( "/me/notifications", response_model=Envelope[list[UserNotification]], ) -async def list_user_notifications(): - ... +async def list_user_notifications(): ... @router.post( @@ -121,8 +103,7 @@ async def list_user_notifications(): ) async def create_user_notification( _body: UserNotificationCreate, -): - ... +): ... @router.patch( @@ -132,16 +113,14 @@ async def create_user_notification( async def mark_notification_as_read( _path: Annotated[_NotificationPathParams, Depends()], _body: UserNotificationPatch, -): - ... +): ... @router.get( "/me/permissions", response_model=Envelope[list[MyPermissionGet]], ) -async def list_user_permissions(): - ... +async def list_user_permissions(): ... # @@ -154,8 +133,7 @@ async def list_user_permissions(): response_model=Envelope[list[UserGet]], description="Search among users who are publicly visible to the caller (i.e., me) based on their privacy settings.", ) -async def search_users(_body: UsersSearch): - ... +async def search_users(_body: UsersSearch): ... # @@ -171,7 +149,7 @@ async def search_users(_body: UsersSearch): tags=_extra_tags, ) async def search_users_for_admin( - _query: Annotated[UsersForAdminSearchQueryParams, Depends()] + _query: Annotated[UsersForAdminSearchQueryParams, Depends()], ): # NOTE: see `Search` in `Common Custom Methods` in https://cloud.google.com/apis/design/custom_methods ... @@ -182,5 +160,4 @@ async def search_users_for_admin( response_model=Envelope[UserForAdminGet], tags=_extra_tags, ) -async def pre_register_user_for_admin(_body: PreRegisteredUserGet): - ... +async def pre_register_user_for_admin(_body: PreRegisteredUserGet): ... diff --git a/services/web/server/src/simcore_service_webserver/users/_users_rest.py b/services/web/server/src/simcore_service_webserver/users/_users_rest.py index e3c143cd7cf4..e89814e5e2db 100644 --- a/services/web/server/src/simcore_service_webserver/users/_users_rest.py +++ b/services/web/server/src/simcore_service_webserver/users/_users_rest.py @@ -113,9 +113,6 @@ async def get_my_profile(request: web.Request) -> web.Response: @routes.patch(f"/{API_VTAG}/me", name="update_my_profile") -@routes.put( - f"/{API_VTAG}/me", name="replace_my_profile" # deprecated. Use patch instead -) @login_required @permission_required("user.profile.update") @_handle_users_exceptions From 8ea9dfdecc515c8add9234d9e8e07ca51283360f Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:57:39 +0100 Subject: [PATCH 26/50] update OAS --- .../api/v0/openapi.yaml | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 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 b6e931af5338..9ffec790af1d 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 @@ -1180,22 +1180,6 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_MyProfileGet_' - put: - tags: - - users - summary: Replace My Profile - description: Use PATCH instead - operationId: replace_my_profile - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/MyProfilePatch' - required: true - responses: - '204': - description: Successful Response - deprecated: true patch: tags: - users @@ -11734,6 +11718,9 @@ components: last_name: Crespo MyProfilePrivacyGet: properties: + hideUsername: + type: boolean + title: Hideusername hideFullname: type: boolean title: Hidefullname @@ -11742,11 +11729,17 @@ components: title: Hideemail type: object required: + - hideUsername - hideFullname - hideEmail title: MyProfilePrivacyGet MyProfilePrivacyPatch: properties: + hideUsername: + anyOf: + - type: boolean + - type: 'null' + title: Hideusername hideFullname: anyOf: - type: boolean From 4c3f5bed568ecba821a992e0c5dbb02923a7d252 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:57:44 +0100 Subject: [PATCH 27/50] =?UTF-8?q?services/webserver=20api=20version:=200.6?= =?UTF-8?q?1.3=20=E2=86=92=200.61.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/web/server/VERSION | 2 +- services/web/server/setup.cfg | 2 +- .../server/src/simcore_service_webserver/api/v0/openapi.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/web/server/VERSION b/services/web/server/VERSION index d1952dc561e0..bf54d53ec26d 100644 --- a/services/web/server/VERSION +++ b/services/web/server/VERSION @@ -1 +1 @@ -0.61.3 +0.61.4 diff --git a/services/web/server/setup.cfg b/services/web/server/setup.cfg index 9d731c416393..ccbfa6b24c9e 100644 --- a/services/web/server/setup.cfg +++ b/services/web/server/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.61.3 +current_version = 0.61.4 commit = True message = services/webserver api version: {current_version} → {new_version} tag = False 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 9ffec790af1d..6facdd9ddf15 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 @@ -2,7 +2,7 @@ openapi: 3.1.0 info: title: simcore-service-webserver description: Main service with an interface (http-API & websockets) to the web front-end - version: 0.61.3 + version: 0.61.4 servers: - url: '' description: webserver From e37dc5f7db7d35789a30e92a1b60cc675c091c1d Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 16:18:06 +0100 Subject: [PATCH 28/50] wrong import --- .../src/models_library/api_schemas_webserver/users.py | 5 +---- 1 file changed, 1 insertion(+), 4 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 a2f162602cce..1facf8bb1e9e 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 @@ -17,7 +17,6 @@ field_validator, ) from pydantic.config import JsonDict -from simcore_postgres_database.utils_users import MIN_USERNAME_LEN from ..basic_types import IDStr from ..emails import LowerCaseEmailStr @@ -155,9 +154,7 @@ def from_domain_model( class MyProfilePatch(InputSchemaWithoutCamelCase): first_name: FirstNameStr | None = None last_name: LastNameStr | None = None - user_name: Annotated[ - IDStr | None, Field(alias="userName", min_length=MIN_USERNAME_LEN) - ] = None + user_name: Annotated[IDStr | None, Field(alias="userName", min_length=4)] = None privacy: MyProfilePrivacyPatch | None = None From a0c3bb2e156f987c5f0ff114640e7afe0745daf9 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 16:19:19 +0100 Subject: [PATCH 29/50] wrong example --- services/web/server/tests/unit/isolated/test_users_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/server/tests/unit/isolated/test_users_models.py b/services/web/server/tests/unit/isolated/test_users_models.py index 99fdc7f4febb..5141cae0a461 100644 --- a/services/web/server/tests/unit/isolated/test_users_models.py +++ b/services/web/server/tests/unit/isolated/test_users_models.py @@ -78,7 +78,7 @@ def test_parsing_output_of_get_user_profile(): "last_name": "", "role": "Guest", "gravatar_id": "9d5e02c75fcd4bce1c8861f219f7f8a5", - "privacy": {"hide_email": True, "hide_fullname": False}, + "privacy": {"hide_email": True, "hide_fullname": False, "hide_username": False}, "groups": { "me": { "gid": 2, From f07de3f53132d027892a76b117fe57b8bc9a815c Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 16:27:37 +0100 Subject: [PATCH 30/50] fixes test --- services/web/server/tests/unit/isolated/test_users_models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/web/server/tests/unit/isolated/test_users_models.py b/services/web/server/tests/unit/isolated/test_users_models.py index 5141cae0a461..e1dca917ee72 100644 --- a/services/web/server/tests/unit/isolated/test_users_models.py +++ b/services/web/server/tests/unit/isolated/test_users_models.py @@ -33,7 +33,9 @@ def fake_profile_get(faker: Faker) -> MyProfileGet: user_name=fake_profile["username"], login=fake_profile["mail"], role="USER", - privacy=MyProfilePrivacyGet(hide_fullname=True, hide_email=True), + privacy=MyProfilePrivacyGet( + hide_fullname=True, hide_email=True, hide_username=False + ), preferences={}, ) From e9735c4680e3c4ed73c5da8a4b5560cf031fcb45 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Mon, 24 Mar 2025 09:32:26 +0100 Subject: [PATCH 31/50] minor --- .../client/source/class/osparc/desktop/WorkbenchView.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js b/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js index 2f40fee70b64..e10cc5f62fdd 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js +++ b/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js @@ -442,7 +442,7 @@ qx.Class.define("osparc.desktop.WorkbenchView", { appearance: "form-button-outlined", label: this.tr("App Mode"), toolTipText: this.tr("Start App Mode"), - icon: "@FontAwesome5Solid/play/14", + icon: osparc.dashboard.CardBase.MODE_APP, marginRight: 10, marginTop: 7, ...osparc.navigation.NavigationBar.BUTTON_OPTIONS @@ -837,7 +837,7 @@ qx.Class.define("osparc.desktop.WorkbenchView", { const startAppBtn = this.__startAppButton = new qx.ui.form.Button().set({ label: this.tr("Start"), - icon: "@FontAwesome5Solid/play/14", + icon: osparc.dashboard.CardBase.MODE_APP, toolTipText: this.tr("Start App Mode"), height: buttonsHeight }); From 6fca95503ad7668d99891ca37a4fcf6c38ab52ee Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Mon, 24 Mar 2025 09:41:20 +0100 Subject: [PATCH 32/50] more startup calls --- tests/e2e/tests/startupCalls.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/e2e/tests/startupCalls.js b/tests/e2e/tests/startupCalls.js index cadffad1ef74..aca31c32e6f3 100644 --- a/tests/e2e/tests/startupCalls.js +++ b/tests/e2e/tests/startupCalls.js @@ -11,6 +11,9 @@ module.exports = { const responses = { me: null, + tags: null, + tasks: null, + uiConfig: null, studies: null, templates: null, services: null, @@ -23,6 +26,12 @@ module.exports = { const url = response.url(); if (url.endsWith('/me')) { responses.me = response.json(); + } else if (url.includes('/tags')) { + responses.tags = response.json(); + } else if (url.includes('/tasks')) { + responses.tasks = response.json(); + } else if (url.includes('/ui')) { + responses.uiConfig = response.json(); } else if (url.includes('projects?type=user')) { responses.studies = response.json(); } else if (url.includes('projects?type=template')) { @@ -52,6 +61,24 @@ module.exports = { expect(responseEnv.data["login"]).toBe(user); }, ourTimeout); + test('Tags', async () => { + const responseEnv = await responses.tags; + expect(Array.isArray(responseEnv.data)).toBeTruthy(); + }, ourTimeout); + + test('Tasks', async () => { + const responseEnv = await responses.tasks; + expect(Array.isArray(responseEnv.data)).toBeTruthy(); + }, ourTimeout); + + test('UI Config', async () => { + const responseEnv = await responses.uiConfig; + expect(responseEnv.data["productName"]).toBe("osparc"); + const uiConfig = responseEnv.data["ui"]; + const isObject = typeof uiConfig === 'object' && !Array.isArray(uiConfig) && uiConfig !== null; + expect(isObject).toBeTruthy(); + }, ourTimeout); + test('Studies', async () => { const responseEnv = await responses.studies; expect(Array.isArray(responseEnv.data)).toBeTruthy(); From 03a15c3c97aa9573004416526d693cdcece4a53d Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:39:30 +0100 Subject: [PATCH 33/50] missing fields --- .../models_library/api_schemas_webserver/users.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 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 947623fc9567..1b0362d82d1f 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 @@ -16,6 +16,7 @@ ValidationInfo, field_validator, ) +from simcore_postgres_database.utils_users import MIN_USERNAME_LEN from ..basic_types import IDStr from ..emails import LowerCaseEmailStr @@ -46,11 +47,13 @@ class MyProfilePrivacyGet(OutputSchema): + hide_username: bool hide_fullname: bool hide_email: bool class MyProfilePrivacyPatch(InputSchema): + hide_username: bool | None = None hide_fullname: bool | None = None hide_email: bool | None = None @@ -92,7 +95,11 @@ class MyProfileGet(OutputSchemaWithoutCamelCase): "role": "admin", # pre "expirationDate": "2022-09-14", # optional "preferences": {}, - "privacy": {"hide_fullname": 0, "hide_email": 1}, + "privacy": { + "hide_username": 0, + "hide_fullname": 0, + "hide_email": 1, + }, }, ] }, @@ -141,7 +148,9 @@ def from_domain_model( class MyProfilePatch(InputSchemaWithoutCamelCase): first_name: FirstNameStr | None = None last_name: LastNameStr | None = None - user_name: Annotated[IDStr | None, Field(alias="userName", min_length=4)] = None + user_name: Annotated[ + IDStr | None, Field(alias="userName", min_length=MIN_USERNAME_LEN) + ] = None privacy: MyProfilePrivacyPatch | None = None From b67d7995e07cb03bed7ea7bc68a678186151635c Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:39:39 +0100 Subject: [PATCH 34/50] constant --- .../src/simcore_postgres_database/utils_users.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/postgres-database/src/simcore_postgres_database/utils_users.py b/packages/postgres-database/src/simcore_postgres_database/utils_users.py index 7aa5c442d379..c35123c9545b 100644 --- a/packages/postgres-database/src/simcore_postgres_database/utils_users.py +++ b/packages/postgres-database/src/simcore_postgres_database/utils_users.py @@ -27,10 +27,10 @@ class UserNotFoundInRepoError(BaseUserRepoError): # NOTE: see MyProfilePatch.user_name -_MIN_USERNAME_LEN: Final[int] = 4 +MIN_USERNAME_LEN: Final[int] = 4 -def _generate_random_chars(length: int = _MIN_USERNAME_LEN) -> str: +def _generate_random_chars(length: int = MIN_USERNAME_LEN) -> str: """returns `length` random digit character""" return "".join(secrets.choice(string.digits) for _ in range(length)) @@ -42,8 +42,8 @@ def _generate_username_from_email(email: str) -> str: username = re.sub(r"[^a-zA-Z0-9]", "", username).lower() # Ensure the username is at least 4 characters long - if len(username) < _MIN_USERNAME_LEN: - username += _generate_random_chars(length=_MIN_USERNAME_LEN - len(username)) + if len(username) < MIN_USERNAME_LEN: + username += _generate_random_chars(length=MIN_USERNAME_LEN - len(username)) return username From 3c17c2bd5830aac40f316dc7baa429b6dfaf4b4f Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:43:59 +0100 Subject: [PATCH 35/50] refactor --- .../api_schemas_webserver/users.py | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 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 1b0362d82d1f..a2f162602cce 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 @@ -16,6 +16,7 @@ ValidationInfo, field_validator, ) +from pydantic.config import JsonDict from simcore_postgres_database.utils_users import MIN_USERNAME_LEN from ..basic_types import IDStr @@ -82,27 +83,33 @@ class MyProfileGet(OutputSchemaWithoutCamelCase): privacy: MyProfilePrivacyGet preferences: AggregatedPreferences + @staticmethod + def _update_json_schema_extra(schema: JsonDict) -> None: + schema.update( + { + "examples": [ + { + "id": 42, + "login": "bla@foo.com", + "userName": "bla42", + "role": "admin", # pre + "expirationDate": "2022-09-14", # optional + "preferences": {}, + "privacy": { + "hide_username": 0, + "hide_fullname": 0, + "hide_email": 1, + }, + }, + ] + } + ) + model_config = ConfigDict( # NOTE: old models have an hybrid between snake and camel cases! # Should be unified at some point populate_by_name=True, - json_schema_extra={ - "examples": [ - { - "id": 42, - "login": "bla@foo.com", - "userName": "bla42", - "role": "admin", # pre - "expirationDate": "2022-09-14", # optional - "preferences": {}, - "privacy": { - "hide_username": 0, - "hide_fullname": 0, - "hide_email": 1, - }, - }, - ] - }, + json_schema_extra=_update_json_schema_extra, ) @field_validator("role", mode="before") From fd4dad6769149adda240362f87e5e291caca5bbc Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:50:32 +0100 Subject: [PATCH 36/50] test --- services/web/server/tests/unit/with_dbs/03/test_users.py | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/server/tests/unit/with_dbs/03/test_users.py b/services/web/server/tests/unit/with_dbs/03/test_users.py index 2d816aa67b1c..c4008f752356 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_users.py +++ b/services/web/server/tests/unit/with_dbs/03/test_users.py @@ -503,6 +503,7 @@ async def test_profile_workflow( assert updated_profile.user_name == "odei123" assert updated_profile.privacy != my_profile.privacy + assert updated_profile.privacy.hide_username == my_profile.privacy.hide_username assert updated_profile.privacy.hide_email == my_profile.privacy.hide_email assert updated_profile.privacy.hide_fullname != my_profile.privacy.hide_fullname From a205478a4e07850ce15bc5b007f4a4065137865f Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:50:46 +0100 Subject: [PATCH 37/50] retire put --- api/specs/web-server/_users.py | 51 +++++-------------- .../users/_users_rest.py | 3 -- 2 files changed, 14 insertions(+), 40 deletions(-) diff --git a/api/specs/web-server/_users.py b/api/specs/web-server/_users.py index 89d5eaaba2f9..d0d733a01e34 100644 --- a/api/specs/web-server/_users.py +++ b/api/specs/web-server/_users.py @@ -39,26 +39,14 @@ "/me", response_model=Envelope[MyProfileGet], ) -async def get_my_profile(): - ... +async def get_my_profile(): ... @router.patch( "/me", status_code=status.HTTP_204_NO_CONTENT, ) -async def update_my_profile(_body: MyProfilePatch): - ... - - -@router.put( - "/me", - status_code=status.HTTP_204_NO_CONTENT, - deprecated=True, - description="Use PATCH instead", -) -async def replace_my_profile(_body: MyProfilePatch): - ... +async def update_my_profile(_body: MyProfilePatch): ... @router.patch( @@ -68,16 +56,14 @@ async def replace_my_profile(_body: MyProfilePatch): async def set_frontend_preference( preference_id: PreferenceIdentifier, _body: PatchRequestBody, -): - ... +): ... @router.get( "/me/tokens", response_model=Envelope[list[MyTokenGet]], ) -async def list_tokens(): - ... +async def list_tokens(): ... @router.post( @@ -85,8 +71,7 @@ async def list_tokens(): response_model=Envelope[MyTokenGet], status_code=status.HTTP_201_CREATED, ) -async def create_token(_body: MyTokenCreate): - ... +async def create_token(_body: MyTokenCreate): ... @router.get( @@ -95,24 +80,21 @@ async def create_token(_body: MyTokenCreate): ) async def get_token( _path: Annotated[_TokenPathParams, Depends()], -): - ... +): ... @router.delete( "/me/tokens/{service}", status_code=status.HTTP_204_NO_CONTENT, ) -async def delete_token(_path: Annotated[_TokenPathParams, Depends()]): - ... +async def delete_token(_path: Annotated[_TokenPathParams, Depends()]): ... @router.get( "/me/notifications", response_model=Envelope[list[UserNotification]], ) -async def list_user_notifications(): - ... +async def list_user_notifications(): ... @router.post( @@ -121,8 +103,7 @@ async def list_user_notifications(): ) async def create_user_notification( _body: UserNotificationCreate, -): - ... +): ... @router.patch( @@ -132,16 +113,14 @@ async def create_user_notification( async def mark_notification_as_read( _path: Annotated[_NotificationPathParams, Depends()], _body: UserNotificationPatch, -): - ... +): ... @router.get( "/me/permissions", response_model=Envelope[list[MyPermissionGet]], ) -async def list_user_permissions(): - ... +async def list_user_permissions(): ... # @@ -154,8 +133,7 @@ async def list_user_permissions(): response_model=Envelope[list[UserGet]], description="Search among users who are publicly visible to the caller (i.e., me) based on their privacy settings.", ) -async def search_users(_body: UsersSearch): - ... +async def search_users(_body: UsersSearch): ... # @@ -171,7 +149,7 @@ async def search_users(_body: UsersSearch): tags=_extra_tags, ) async def search_users_for_admin( - _query: Annotated[UsersForAdminSearchQueryParams, Depends()] + _query: Annotated[UsersForAdminSearchQueryParams, Depends()], ): # NOTE: see `Search` in `Common Custom Methods` in https://cloud.google.com/apis/design/custom_methods ... @@ -182,5 +160,4 @@ async def search_users_for_admin( response_model=Envelope[UserForAdminGet], tags=_extra_tags, ) -async def pre_register_user_for_admin(_body: PreRegisteredUserGet): - ... +async def pre_register_user_for_admin(_body: PreRegisteredUserGet): ... diff --git a/services/web/server/src/simcore_service_webserver/users/_users_rest.py b/services/web/server/src/simcore_service_webserver/users/_users_rest.py index e3c143cd7cf4..e89814e5e2db 100644 --- a/services/web/server/src/simcore_service_webserver/users/_users_rest.py +++ b/services/web/server/src/simcore_service_webserver/users/_users_rest.py @@ -113,9 +113,6 @@ async def get_my_profile(request: web.Request) -> web.Response: @routes.patch(f"/{API_VTAG}/me", name="update_my_profile") -@routes.put( - f"/{API_VTAG}/me", name="replace_my_profile" # deprecated. Use patch instead -) @login_required @permission_required("user.profile.update") @_handle_users_exceptions From 00eaff1723f856efdbd32523d7042a436250cb09 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:57:39 +0100 Subject: [PATCH 38/50] update OAS --- .../api/v0/openapi.yaml | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 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 b6e931af5338..9ffec790af1d 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 @@ -1180,22 +1180,6 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_MyProfileGet_' - put: - tags: - - users - summary: Replace My Profile - description: Use PATCH instead - operationId: replace_my_profile - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/MyProfilePatch' - required: true - responses: - '204': - description: Successful Response - deprecated: true patch: tags: - users @@ -11734,6 +11718,9 @@ components: last_name: Crespo MyProfilePrivacyGet: properties: + hideUsername: + type: boolean + title: Hideusername hideFullname: type: boolean title: Hidefullname @@ -11742,11 +11729,17 @@ components: title: Hideemail type: object required: + - hideUsername - hideFullname - hideEmail title: MyProfilePrivacyGet MyProfilePrivacyPatch: properties: + hideUsername: + anyOf: + - type: boolean + - type: 'null' + title: Hideusername hideFullname: anyOf: - type: boolean From 5e0789f20c5af356780b95504d9a41c501962708 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:57:44 +0100 Subject: [PATCH 39/50] =?UTF-8?q?services/webserver=20api=20version:=200.6?= =?UTF-8?q?1.3=20=E2=86=92=200.61.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/web/server/VERSION | 2 +- services/web/server/setup.cfg | 2 +- .../server/src/simcore_service_webserver/api/v0/openapi.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/web/server/VERSION b/services/web/server/VERSION index d1952dc561e0..bf54d53ec26d 100644 --- a/services/web/server/VERSION +++ b/services/web/server/VERSION @@ -1 +1 @@ -0.61.3 +0.61.4 diff --git a/services/web/server/setup.cfg b/services/web/server/setup.cfg index 9d731c416393..ccbfa6b24c9e 100644 --- a/services/web/server/setup.cfg +++ b/services/web/server/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.61.3 +current_version = 0.61.4 commit = True message = services/webserver api version: {current_version} → {new_version} tag = False 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 9ffec790af1d..6facdd9ddf15 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 @@ -2,7 +2,7 @@ openapi: 3.1.0 info: title: simcore-service-webserver description: Main service with an interface (http-API & websockets) to the web front-end - version: 0.61.3 + version: 0.61.4 servers: - url: '' description: webserver From 06e7f4cf9c6c9a61919b28434e41995aa57b1f46 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 16:18:06 +0100 Subject: [PATCH 40/50] wrong import --- .../src/models_library/api_schemas_webserver/users.py | 5 +---- 1 file changed, 1 insertion(+), 4 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 a2f162602cce..1facf8bb1e9e 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 @@ -17,7 +17,6 @@ field_validator, ) from pydantic.config import JsonDict -from simcore_postgres_database.utils_users import MIN_USERNAME_LEN from ..basic_types import IDStr from ..emails import LowerCaseEmailStr @@ -155,9 +154,7 @@ def from_domain_model( class MyProfilePatch(InputSchemaWithoutCamelCase): first_name: FirstNameStr | None = None last_name: LastNameStr | None = None - user_name: Annotated[ - IDStr | None, Field(alias="userName", min_length=MIN_USERNAME_LEN) - ] = None + user_name: Annotated[IDStr | None, Field(alias="userName", min_length=4)] = None privacy: MyProfilePrivacyPatch | None = None From eaa357e0b0e1369edf54f6ed4935d1e1ff7dccb1 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 16:19:19 +0100 Subject: [PATCH 41/50] wrong example --- services/web/server/tests/unit/isolated/test_users_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/server/tests/unit/isolated/test_users_models.py b/services/web/server/tests/unit/isolated/test_users_models.py index 99fdc7f4febb..5141cae0a461 100644 --- a/services/web/server/tests/unit/isolated/test_users_models.py +++ b/services/web/server/tests/unit/isolated/test_users_models.py @@ -78,7 +78,7 @@ def test_parsing_output_of_get_user_profile(): "last_name": "", "role": "Guest", "gravatar_id": "9d5e02c75fcd4bce1c8861f219f7f8a5", - "privacy": {"hide_email": True, "hide_fullname": False}, + "privacy": {"hide_email": True, "hide_fullname": False, "hide_username": False}, "groups": { "me": { "gid": 2, From 3de59493e90ab8d984c5c579d6762433cad7fb76 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 21 Mar 2025 16:27:37 +0100 Subject: [PATCH 42/50] fixes test --- services/web/server/tests/unit/isolated/test_users_models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/web/server/tests/unit/isolated/test_users_models.py b/services/web/server/tests/unit/isolated/test_users_models.py index 5141cae0a461..e1dca917ee72 100644 --- a/services/web/server/tests/unit/isolated/test_users_models.py +++ b/services/web/server/tests/unit/isolated/test_users_models.py @@ -33,7 +33,9 @@ def fake_profile_get(faker: Faker) -> MyProfileGet: user_name=fake_profile["username"], login=fake_profile["mail"], role="USER", - privacy=MyProfilePrivacyGet(hide_fullname=True, hide_email=True), + privacy=MyProfilePrivacyGet( + hide_fullname=True, hide_email=True, hide_username=False + ), preferences={}, ) From ea59980287dba006ed115005c83eaa462da92343 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 24 Mar 2025 10:26:18 +0100 Subject: [PATCH 43/50] @odeimaiz review: missing field --- .../src/simcore_service_webserver/users/_common/models.py | 1 + services/web/server/tests/unit/isolated/test_users_models.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/services/web/server/src/simcore_service_webserver/users/_common/models.py b/services/web/server/src/simcore_service_webserver/users/_common/models.py index 513d8bed1029..967f010d0b06 100644 --- a/services/web/server/src/simcore_service_webserver/users/_common/models.py +++ b/services/web/server/src/simcore_service_webserver/users/_common/models.py @@ -55,6 +55,7 @@ class ToUserUpdateDB(BaseModel): first_name: str | None = None last_name: str | None = None + privacy_hide_username: bool | None = None privacy_hide_fullname: bool | None = None privacy_hide_email: bool | None = None diff --git a/services/web/server/tests/unit/isolated/test_users_models.py b/services/web/server/tests/unit/isolated/test_users_models.py index e1dca917ee72..bf491fe490a6 100644 --- a/services/web/server/tests/unit/isolated/test_users_models.py +++ b/services/web/server/tests/unit/isolated/test_users_models.py @@ -127,7 +127,7 @@ def test_mapping_update_models_from_rest_to_db(): { "first_name": "foo", "userName": "foo1234", - "privacy": {"hideFullname": False}, + "privacy": {"hideFullname": False, "hideUsername": True}, } ) @@ -139,6 +139,7 @@ def test_mapping_update_models_from_rest_to_db(): "first_name": "foo", "name": "foo1234", "privacy_hide_fullname": False, + "privacy_hide_username": False, } From 1e0eaff4ad970ce2c61c89dec7788ea5cbde4530 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Mon, 24 Mar 2025 10:29:44 +0100 Subject: [PATCH 44/50] better practices --- .../osparc/desktop/account/ProfilePage.js | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js index d7968b5af83c..89d5a53f2196 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js +++ b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js @@ -117,12 +117,12 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { readOnly: true }); - const form = this.__userProfileForm = new qx.ui.form.Form(); - form.add(username, "Username", null, "username"); - form.add(firstName, "First Name", null, "firstName"); - form.add(lastName, "Last Name", null, "lastName"); - form.add(email, "Email", null, "email"); - const singleWithIcon = this.__userProfileRenderer = new osparc.ui.form.renderer.SingleWithIcon(form); + const profileForm = this.__userProfileForm = new qx.ui.form.Form(); + profileForm.add(username, "Username", null, "username"); + profileForm.add(firstName, "First Name", null, "firstName"); + profileForm.add(lastName, "Last Name", null, "lastName"); + profileForm.add(email, "Email", null, "email"); + const singleWithIcon = this.__userProfileRenderer = new osparc.ui.form.renderer.SingleWithIcon(profileForm); box.add(singleWithIcon); const expirationLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)).set({ @@ -180,14 +180,16 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { namesValidator.add(firstName, qx.util.Validate.regExp(/[^\.\d]+/), this.tr("Avoid dots or numbers in text")); namesValidator.add(lastName, qx.util.Validate.regExp(/^$|[^\.\d]+/), this.tr("Avoid dots or numbers in text")); // allow also empty last name - const updateBtn = new qx.ui.form.Button("Update Profile").set({ + const updateProfileBtn = new qx.ui.form.Button().set({ + label: this.tr("Update Profile"), appearance: "form-button", alignX: "right", - allowGrowX: false + allowGrowX: false, + enabled: false, }); - box.add(updateBtn); + box.add(updateProfileBtn); - updateBtn.addListener("execute", () => { + updateProfileBtn.addListener("execute", () => { if (!osparc.data.Permissions.getInstance().canDo("user.user.update", true)) { this.__resetUserData(); return; @@ -229,13 +231,13 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { __createPrivacySection: function() { // binding to a model - const raw = { + const defaultModel = { "hideUsername": false, "hideFullname": true, "hideEmail": true, }; - const privacyModel = this.__userPrivacyModel = qx.data.marshal.Json.createModel(raw); + const privacyModel = this.__userPrivacyModel = qx.data.marshal.Json.createModel(defaultModel, true); const box = osparc.ui.window.TabbedView.createSectionBox(this.tr("Privacy")); box.set({ @@ -247,30 +249,36 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { box.add(label); const hideUsername = new qx.ui.form.CheckBox().set({ - value: false + value: defaultModel.hideUsername }); const hideFullname = new qx.ui.form.CheckBox().set({ - value: true + value: defaultModel.hideFullname }); const hideEmail = new qx.ui.form.CheckBox().set({ - value: true + value: defaultModel.hideEmail }); - const form = new qx.ui.form.Form(); - form.add(hideUsername, "Hide Username", null, "hideUsername"); - form.add(hideFullname, "Hide Full Name", null, "hideFullname"); - form.add(hideEmail, "Hide Email", null, "hideEmail"); - box.add(new qx.ui.form.renderer.Single(form)); + const privacyForm = new qx.ui.form.Form(); + privacyForm.add(hideUsername, "Hide Username", null, "hideUsername"); + privacyForm.add(hideFullname, "Hide Full Name", null, "hideFullname"); + privacyForm.add(hideEmail, "Hide Email", null, "hideEmail"); + box.add(new qx.ui.form.renderer.Single(privacyForm)); - const controller = new qx.data.controller.Object(privacyModel); - controller.addTarget(hideUsername, "value", "hideUsername", true); - controller.addTarget(hideFullname, "value", "hideFullname", true); - controller.addTarget(hideEmail, "value", "hideEmail", true); + const privacyModelCtrl = new qx.data.controller.Object(privacyModel); + privacyModelCtrl.addTarget(hideUsername, "value", "hideUsername", true); + privacyModelCtrl.addTarget(hideFullname, "value", "hideFullname", true); + privacyModelCtrl.addTarget(hideEmail, "value", "hideEmail", true); - const privacyBtn = new qx.ui.form.Button("Update Privacy").set({ + privacyModelCtrl.addListener("changeTarget", () => { + console.log("form changeModel"); + }); + + const privacyBtn = new qx.ui.form.Button().set({ + label: this.tr("Update Privacy"), appearance: "form-button", alignX: "right", - allowGrowX: false + allowGrowX: false, + enabled: false, }); box.add(privacyBtn); privacyBtn.addListener("execute", () => { @@ -334,15 +342,15 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { hideFullname, hideEmail, ] - const evaluateWarningMessage = () => { + const valuesChanged = () => { if (privacyFields.every(privacyField => privacyField.getValue())) { optOutMessage.show(); } else { optOutMessage.exclude(); } }; - evaluateWarningMessage(); - privacyFields.forEach(privacyField => privacyField.addListener("changeValue", () => evaluateWarningMessage())); + valuesChanged(); + privacyFields.forEach(privacyField => privacyField.addListener("changeValue", () => valuesChanged())); return box; }, From d63516b33c66a249f04d3cb2ccbe5a71a556d6b6 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Mon, 24 Mar 2025 10:30:25 +0100 Subject: [PATCH 45/50] minor --- .../client/source/class/osparc/desktop/account/ProfilePage.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js index 89d5a53f2196..83172e3e2661 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js +++ b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js @@ -31,6 +31,9 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { this._setLayout(new qx.ui.layout.VBox(15)); + this.__userProfileData = {}; + this.__userPrivacyData = {}; + this.__fetchProfile(); this._add(this.__createProfileUser()); From f0f4e0f648092e7ac29979b0ee44f74e814c69b7 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Mon, 24 Mar 2025 10:35:40 +0100 Subject: [PATCH 46/50] enable button --- .../osparc/desktop/account/ProfilePage.js | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js index 83172e3e2661..95caf599eec1 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js +++ b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js @@ -94,6 +94,7 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { }; this.__userProfileRenderer.setIcons(icons); } + this.__updatePrivacyBtn.setEnabled(false); }, __createProfileUser: function() { @@ -272,19 +273,15 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { privacyModelCtrl.addTarget(hideFullname, "value", "hideFullname", true); privacyModelCtrl.addTarget(hideEmail, "value", "hideEmail", true); - privacyModelCtrl.addListener("changeTarget", () => { - console.log("form changeModel"); - }); - - const privacyBtn = new qx.ui.form.Button().set({ + const updatePrivacyBtn = this.__updatePrivacyBtn = new qx.ui.form.Button().set({ label: this.tr("Update Privacy"), appearance: "form-button", alignX: "right", allowGrowX: false, enabled: false, }); - box.add(privacyBtn); - privacyBtn.addListener("execute", () => { + box.add(updatePrivacyBtn); + updatePrivacyBtn.addListener("execute", () => { if (!osparc.data.Permissions.getInstance().canDo("user.user.update", true)) { this.__resetPrivacyData(); return; @@ -345,15 +342,21 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { hideFullname, hideEmail, ] - const valuesChanged = () => { + const valueChanged = () => { + const anyChanged = + hideUsername.getValue() !== this.__userPrivacyData["hideUsername"] || + hideFullname.getValue() !== this.__userPrivacyData["hideFullname"] || + hideEmail.getValue() !== this.__userPrivacyData["hideEmail"]; + updatePrivacyBtn.setEnabled(anyChanged); + if (privacyFields.every(privacyField => privacyField.getValue())) { optOutMessage.show(); } else { optOutMessage.exclude(); } }; - valuesChanged(); - privacyFields.forEach(privacyField => privacyField.addListener("changeValue", () => valuesChanged())); + valueChanged(); + privacyFields.forEach(privacyField => privacyField.addListener("changeValue", () => valueChanged())); return box; }, From f5107dc2ab31c668b92b87b0dd0baa2627c3bb31 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Mon, 24 Mar 2025 10:44:07 +0100 Subject: [PATCH 47/50] profileFields --- .../osparc/desktop/account/ProfilePage.js | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js index 95caf599eec1..07a32d3358b5 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js +++ b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js @@ -49,8 +49,10 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { __userProfileData: null, __userProfileModel: null, __userProfileRenderer: null, + __updateProfileBtn: null, __userPrivacyData: null, __userPrivacyModel: null, + __updatePrivacyBtn: null, __userProfileForm: null, __fetchProfile: function() { @@ -73,6 +75,7 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { "expirationDate": data["expirationDate"] || null, }); } + this.__updateProfileBtn.setEnabled(false); }, __setDataToPrivacy: function(privacyData) { @@ -184,7 +187,7 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { namesValidator.add(firstName, qx.util.Validate.regExp(/[^\.\d]+/), this.tr("Avoid dots or numbers in text")); namesValidator.add(lastName, qx.util.Validate.regExp(/^$|[^\.\d]+/), this.tr("Avoid dots or numbers in text")); // allow also empty last name - const updateProfileBtn = new qx.ui.form.Button().set({ + const updateProfileBtn = this.__updateProfileBtn = new qx.ui.form.Button().set({ label: this.tr("Update Profile"), appearance: "form-button", alignX: "right", @@ -200,7 +203,7 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { } const patchData = {}; - if (this.__userProfileData["username"] !== model.getUsername()) { + if (this.__userProfileData["userName"] !== model.getUsername()) { patchData["userName"] = model.getUsername(); } if (this.__userProfileData["first_name"] !== model.getFirstName()) { @@ -230,6 +233,21 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { } }); + const profileFields = [ + username, + firstName, + lastName, + ] + const valueChanged = () => { + const anyChanged = + username.getValue() !== this.__userProfileData["userName"] || + firstName.getValue() !== this.__userProfileData["first_name"] || + lastName.getValue() !== this.__userProfileData["last_name"]; + updateProfileBtn.setEnabled(anyChanged); + }; + valueChanged(); + profileFields.forEach(privacyField => privacyField.addListener("changeValue", () => valueChanged())); + return box; }, @@ -337,6 +355,7 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { }); optOutMessage.getChildControl("icon").setTextColor("warning-yellow") box.add(optOutMessage); + const privacyFields = [ hideUsername, hideFullname, From 6ea4ba7f78bc8b6dc618d5561e54d9bad93b6ee3 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Mon, 24 Mar 2025 11:35:55 +0100 Subject: [PATCH 48/50] minor --- tests/e2e/tests/startupCalls.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/e2e/tests/startupCalls.js b/tests/e2e/tests/startupCalls.js index aca31c32e6f3..7157fa690824 100644 --- a/tests/e2e/tests/startupCalls.js +++ b/tests/e2e/tests/startupCalls.js @@ -26,11 +26,11 @@ module.exports = { const url = response.url(); if (url.endsWith('/me')) { responses.me = response.json(); - } else if (url.includes('/tags')) { + } else if (url.endsWith('/tags')) { responses.tags = response.json(); - } else if (url.includes('/tasks')) { + } else if (url.endsWith('/tasks')) { responses.tasks = response.json(); - } else if (url.includes('/ui')) { + } else if (url.endsWith('/ui')) { responses.uiConfig = response.json(); } else if (url.includes('projects?type=user')) { responses.studies = response.json(); From 66e3490b844fed3021b472199cdab84e511a01a0 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Mon, 24 Mar 2025 11:54:46 +0100 Subject: [PATCH 49/50] do not check tasks --- tests/e2e/tests/startupCalls.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/e2e/tests/startupCalls.js b/tests/e2e/tests/startupCalls.js index 7157fa690824..78d080c1367c 100644 --- a/tests/e2e/tests/startupCalls.js +++ b/tests/e2e/tests/startupCalls.js @@ -66,10 +66,12 @@ module.exports = { expect(Array.isArray(responseEnv.data)).toBeTruthy(); }, ourTimeout); + /* test('Tasks', async () => { const responseEnv = await responses.tasks; expect(Array.isArray(responseEnv.data)).toBeTruthy(); }, ourTimeout); + */ test('UI Config', async () => { const responseEnv = await responses.uiConfig; From 10f741dad9707d53c42a996bd26b910de1f37715 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Mon, 24 Mar 2025 12:23:55 +0100 Subject: [PATCH 50/50] bad merge --- services/web/server/tests/unit/isolated/test_users_models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/services/web/server/tests/unit/isolated/test_users_models.py b/services/web/server/tests/unit/isolated/test_users_models.py index a49967ccef60..e61f543e2113 100644 --- a/services/web/server/tests/unit/isolated/test_users_models.py +++ b/services/web/server/tests/unit/isolated/test_users_models.py @@ -139,7 +139,6 @@ def test_mapping_update_models_from_rest_to_db(): "first_name": "foo", "name": "foo1234", "privacy_hide_fullname": False, - "privacy_hide_username": False, "privacy_hide_username": True, }