From a1b471b042051522d394429b6b198e85c536924c Mon Sep 17 00:00:00 2001 From: Paula-Kli Date: Tue, 17 Oct 2023 11:51:09 +0200 Subject: [PATCH] feat: Add user profile page Users can now look at common projects with another user or have a look at their personal profile page. --- backend/capellacollab/users/routes.py | 77 ++++++++++++++--- backend/tests/users/test_users.py | 84 +++++++++++++++++++ frontend/src/app/app-routing.module.ts | 6 ++ frontend/src/app/app.module.ts | 2 + .../app/general/header/header.component.html | 10 ++- .../nav-bar-menu/nav-bar-menu.component.html | 9 ++ .../project-user-settings.component.html | 8 +- .../src/app/services/user/user.service.ts | 11 +++ .../user-settings.component.html | 59 +++++++++---- .../src/app/settings/settings.component.html | 2 +- .../users-profile/users-profile.component.css | 4 + .../users-profile.component.html | 44 ++++++++++ .../users-profile/users-profile.component.ts | 68 +++++++++++++++ 13 files changed, 354 insertions(+), 30 deletions(-) create mode 100644 backend/tests/users/test_users.py create mode 100644 frontend/src/app/users/users-profile/users-profile.component.css create mode 100644 frontend/src/app/users/users-profile/users-profile.component.html create mode 100644 frontend/src/app/users/users-profile/users-profile.component.ts diff --git a/backend/capellacollab/users/routes.py b/backend/capellacollab/users/routes.py index 1839a76eea..f5f3a64e62 100644 --- a/backend/capellacollab/users/routes.py +++ b/backend/capellacollab/users/routes.py @@ -1,13 +1,18 @@ # SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors # SPDX-License-Identifier: Apache-2.0 +import logging from collections import abc import fastapi +from fastapi import status from sqlalchemy import orm from capellacollab.core import database +from capellacollab.core import logging as core_logging from capellacollab.core.authentication import injectables as auth_injectables +from capellacollab.projects import crud as projects_crud +from capellacollab.projects import models as projects_models from capellacollab.projects.users import crud as projects_users_crud from capellacollab.sessions import routes as session_routes from capellacollab.users.events import crud as events_crud @@ -33,19 +38,28 @@ def get_current_user( return user -@router.get( - "/{user_id}", - response_model=models.User, - dependencies=[ - fastapi.Depends( - auth_injectables.RoleVerification(required_role=models.Role.ADMIN) - ) - ], -) +@router.get("/{user_id}", response_model=models.User) def get_user( + own_username: str = fastapi.Depends(auth_injectables.get_username), + own_user: models.DatabaseUser = fastapi.Depends(injectables.get_own_user), user: models.DatabaseUser = fastapi.Depends(injectables.get_existing_user), + db: orm.Session = fastapi.Depends(database.get_db), ) -> models.DatabaseUser: - return user + if ( + auth_injectables.RoleVerification( + required_role=models.Role.ADMIN, verify=False + )(own_username, db) + or len(get_common_projects_for_users(own_user, user, db)) > 0 + or user.id == own_user.id + ): + return user + else: + raise fastapi.HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail={ + "reason": "You have to have at least one project in common to get this information.", + }, + ) @router.get( @@ -84,6 +98,28 @@ def create_user( return created_user +@router.get( + "/{user_id}/common-projects", + response_model=list[projects_models.Project], + tags=["Projects"], +) +def get_common_projects( + user_for_common_projects: models.DatabaseUser = fastapi.Depends( + injectables.get_existing_user + ), + user: models.DatabaseUser = fastapi.Depends(injectables.get_own_user), + db: orm.Session = fastapi.Depends(database.get_db), + log: logging.LoggerAdapter = fastapi.Depends( + core_logging.get_request_logger + ), +) -> set[projects_models.DatabaseProject]: + projects = get_common_projects_for_users( + user, user_for_common_projects, db + ) + log.info("Fetching the following projects: %s", projects) + return projects + + @router.patch( "/{user_id}/roles", response_model=models.User, @@ -134,6 +170,27 @@ def delete_user( crud.delete_user(db, user) +def get_common_projects_for_users( + user_one: models.DatabaseUser, + user_two: models.DatabaseUser, + db: orm.Session, +) -> set[projects_models.DatabaseProject]: + first_user_projects = get_projects_for_user(user_one, db) + second_user_projects = get_projects_for_user(user_two, db) + + projects = set(first_user_projects).intersection(set(second_user_projects)) + return projects + + +def get_projects_for_user( + user: models.DatabaseUser, db: orm.Session +) -> list[projects_models.DatabaseProject]: + if user.role != models.Role.ADMIN: + return [association.project for association in user.projects] + else: + return list(projects_crud.get_projects(db)) + + router.include_router(session_routes.users_router, tags=["Users - Sessions"]) router.include_router(events_routes.router, tags=["Users - History"]) router.include_router( diff --git a/backend/tests/users/test_users.py b/backend/tests/users/test_users.py new file mode 100644 index 0000000000..073e0829d2 --- /dev/null +++ b/backend/tests/users/test_users.py @@ -0,0 +1,84 @@ +# SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from fastapi import testclient +from sqlalchemy import orm + +import capellacollab.projects.users.models as projects_users_models +from capellacollab.projects import models as projects_models +from capellacollab.projects.users import crud as projects_users_crud +from capellacollab.users import crud as users_crud +from capellacollab.users import models as users_models + + +def test_get_user_by_id_admin( + client: testclient.TestClient, db: orm.Session, executor_name: str +): + users_crud.create_user(db, executor_name, users_models.Role.ADMIN) + user = users_crud.create_user(db, "test_user") + response = client.get(f"/api/v1/users/{user.id}") + + assert response.status_code == 200 + assert response.json()["name"] == "test_user" + + +@pytest.mark.usefixtures("user") +def test_get_user_by_id_non_admin( + client: testclient.TestClient, db: orm.Session, executor_name: str +): + user = users_crud.create_user(db, "test_user") + response = client.get(f"/api/v1/users/{user.id}") + + assert response.status_code == 403 + + +@pytest.mark.usefixtures("project_user") +def test_get_user_by_id_common_project( + client: testclient.TestClient, + db: orm.Session, + project: projects_models.DatabaseProject, +): + user2 = users_crud.create_user(db, "user2") + projects_users_crud.add_user_to_project( + db, + project, + user2, + role=projects_users_models.ProjectUserRole.USER, + permission=projects_users_models.ProjectUserPermission.WRITE, + ) + + response = client.get(f"/api/v1/users/{user2.id}") + assert response.status_code == 200 + assert response.json()["name"] == "user2" + + +@pytest.mark.usefixtures("user") +def test_get_no_common_projects( + client: testclient.TestClient, db: orm.Session +): + user2 = users_crud.create_user(db, "user2") + response = client.get(f"/api/v1/users/{user2.id}/common-projects") + assert response.status_code == 200 + assert len(response.json()) == 0 + + +@pytest.mark.usefixtures("project_user") +def test_get_common_projects( + client: testclient.TestClient, + db: orm.Session, + project: projects_models.DatabaseProject, +): + user2 = users_crud.create_user(db, "user2") + projects_users_crud.add_user_to_project( + db, + project, + user2, + role=projects_users_models.ProjectUserRole.USER, + permission=projects_users_models.ProjectUserPermission.WRITE, + ) + + response = client.get(f"/api/v1/users/{user2.id}/common-projects") + assert response.status_code == 200 + assert len(response.json()) == 1 + assert response.json()[0]["slug"] == project.slug diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 5dc705859d..fc3bbef665 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -14,6 +14,7 @@ import { EditProjectMetadataComponent } from 'src/app/projects/project-detail/ed import { SessionComponent } from 'src/app/sessions/session/session.component'; import { PipelinesOverviewComponent } from 'src/app/settings/core/pipelines-overview/pipelines-overview.component'; import { BasicAuthTokenComponent } from 'src/app/users/basic-auth-token/basic-auth-token.component'; +import { UsersProfileComponent } from 'src/app/users/users-profile/users-profile.component'; import { EventsComponent } from './events/events.component'; import { AuthComponent } from './general/auth/auth/auth.component'; import { AuthGuardService } from './general/auth/auth-guard/auth-guard.service'; @@ -442,6 +443,11 @@ const routes: Routes = [ data: { breadcrumb: 'Events' }, component: EventsComponent, }, + { + path: 'user', + data: { breadcrumb: (data: Data) => data?.user?.name || 'User' }, + component: UsersProfileComponent, + }, { path: 'tokens', data: { breadcrumb: 'Tokens' }, diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 89dd3a26ea..a604f30c36 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -138,6 +138,7 @@ import { T4CRepoDeletionDialogComponent } from './settings/modelsources/t4c-sett import { T4CSettingsWrapperComponent } from './settings/modelsources/t4c-settings/t4c-settings-wrapper/t4c-settings-wrapper.component'; import { T4CSettingsComponent } from './settings/modelsources/t4c-settings/t4c-settings.component'; import { SettingsComponent } from './settings/settings.component'; +import { UsersProfileComponent } from './users/users-profile/users-profile.component'; @NgModule({ declarations: [ @@ -227,6 +228,7 @@ import { SettingsComponent } from './settings/settings.component'; TriggerPipelineComponent, UserSessionsWrapperComponent, UserSettingsComponent, + UsersProfileComponent, VersionComponent, ViewLogsDialogComponent, ], diff --git a/frontend/src/app/general/header/header.component.html b/frontend/src/app/general/header/header.component.html index 9c18f0a11f..a889a4c372 100644 --- a/frontend/src/app/general/header/header.component.html +++ b/frontend/src/app/general/header/header.component.html @@ -48,6 +48,14 @@ + + Profile account_circle + - Profile account_circle + Menu menu diff --git a/frontend/src/app/general/nav-bar-menu/nav-bar-menu.component.html b/frontend/src/app/general/nav-bar-menu/nav-bar-menu.component.html index 3e9c89f971..af5e7023d3 100644 --- a/frontend/src/app/general/nav-bar-menu/nav-bar-menu.component.html +++ b/frontend/src/app/general/nav-bar-menu/nav-bar-menu.component.html @@ -31,6 +31,15 @@ open_in_new + + Profile + + Project members ) " > - - +