Skip to content

Commit

Permalink
feat: Add user profile page
Browse files Browse the repository at this point in the history
Users can now look at common projects with another user or have a look at their personal profile page.
  • Loading branch information
Paula-Kli committed Nov 14, 2023
1 parent 13d568a commit 4ae5a52
Show file tree
Hide file tree
Showing 12 changed files with 353 additions and 29 deletions.
77 changes: 67 additions & 10 deletions backend/capellacollab/users/routes.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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))

Check warning on line 191 in backend/capellacollab/users/routes.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/users/routes.py#L191

Added line #L191 was not covered by tests


router.include_router(session_routes.users_router, tags=["Users - Sessions"])
router.include_router(events_routes.router, tags=["Users - History"])
router.include_router(
Expand Down
84 changes: 84 additions & 0 deletions backend/tests/users/test_users.py
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions frontend/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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' },
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,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: [
Expand Down Expand Up @@ -229,6 +230,7 @@ import { SettingsComponent } from './settings/settings.component';
TriggerPipelineComponent,
UserSessionsWrapperComponent,
UserSettingsComponent,
UsersProfileComponent,
VersionComponent,
ViewLogsDialogComponent,
],
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/app/general/header/header.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@
</a>
</div>
<mat-menu #profileMenu="matMenu">
<a
class="profileMenuButton"
mat-menu-item
routerLink="user"
[queryParams]="{ userId: userService.user?.id }"
>
Profile <mat-icon>account_circle</mat-icon>
</a>
<a
*ngIf="userService.user?.role === 'administrator'"
class="profileMenuButton"
Expand Down Expand Up @@ -90,7 +98,7 @@
mat-raised-button
[matMenuTriggerFor]="profileMenu"
>
Profile <mat-icon>account_circle</mat-icon>
Menu <mat-icon>menu</mat-icon>
</button>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@
<mat-icon class="open-in-new">open_in_new</mat-icon></a
>
<mat-divider></mat-divider>
<a
mat-list-item
(click)="navBarService.toggle()"
routerLink="user"
[queryParams]="{ userId: userService.user?.id }"
>
Profile
</a>
<mat-divider></mat-divider>
<a
mat-list-item
(click)="navBarService.toggle()"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ <h3>Current members</h3>
)
"
>
<div class="flex">
<a
class="flex"
[routerLink]="['/user']"
[queryParams]="{ userId: user.user.id }"
>
<div class="ml-0 mr-4 flex items-center">
<mat-icon class="my-auto !h-8 !w-8 text-3xl" mat-list-icon
>account_circle</mat-icon
Expand All @@ -75,7 +79,7 @@ <h3>Current members</h3>
{{ projectUserService.PERMISSIONS[user.permission] }}
</div>
</div>
</div>
</a>
<div class="ml-1 mr-0 flex items-center">
<button
mat-icon-button
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/app/services/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HistoryEvent } from 'src/app/events/service/events.service';
import { Project } from 'src/app/projects/service/project.service';
import { Session } from 'src/app/schemes';
import { environment } from 'src/environments/environment';
import { AuthService } from '../auth/auth.service';
Expand Down Expand Up @@ -50,6 +51,10 @@ export class UserService {
return this.http.get<User>(this.BACKEND_URL_PREFIX + user.id);
}

getUserById(userId: number): Observable<User> {
return this.http.get<User>(this.BACKEND_URL_PREFIX + userId);
}

getCurrentUser(): Observable<User> {
return this.http.get<User>(this.BACKEND_URL_PREFIX + 'current');
}
Expand Down Expand Up @@ -96,6 +101,12 @@ export class UserService {

return true;
}

loadCommonProjects(userId: number): Observable<Project[]> {
return this.http.get<Project[]>(
`${this.BACKEND_URL_PREFIX}${userId}/common-projects`,
);
}
}

export interface User {
Expand Down
Loading

0 comments on commit 4ae5a52

Please sign in to comment.