Skip to content

Commit

Permalink
#705 implement member-detail component (without delete and edit).
Browse files Browse the repository at this point in the history
+ set isWriteable for teams correctly in user requests
  • Loading branch information
janikEndtner committed Jan 5, 2024
1 parent e4de052 commit 734215e
Show file tree
Hide file tree
Showing 14 changed files with 193 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
import ch.puzzle.okr.service.authorization.AuthorizationService;
import ch.puzzle.okr.service.authorization.UserAuthorizationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Expand All @@ -35,9 +37,12 @@ public UserController(UserAuthorizationService userAuthorizationService, Authori
@Content(mediaType = "application/json", schema = @Schema(implementation = UserDto.class)) }), })
@GetMapping
public List<UserDto> getAllUsers() {
return userAuthorizationService.getAllUsers().stream().map(userMapper::toDto).toList();
return userAuthorizationService.getAllUsers().stream()
.map(userMapper::toDto).toList();
}



@Operation(summary = "Get Current User", description = "Get all current logged in user.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Returned current logged in user.", content = {
Expand All @@ -48,4 +53,16 @@ public UserDto getCurrentUser() {
return userMapper.toDto(currentUser);
}

@Operation(summary = "Get User by ID", description = "Get user by given ID.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Returned user", content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = UserDto.class)) }), })
@GetMapping(path = "/{id}")
public UserDto getUserById(
@Parameter(description = "The ID for requested user.", required = true) @PathVariable long id
) {
var user = this.userAuthorizationService.getById(id);
return userMapper.toDto(user);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public KeyResult toKeyResultMetric(KeyResultMetricDto keyResultMetricDto) {
.withId(keyResultMetricDto.id()).withVersion(keyResultMetricDto.version())
.withObjective(objectiveBusinessService.getEntityById(keyResultMetricDto.objective().id()))
.withTitle(keyResultMetricDto.title()).withDescription(keyResultMetricDto.description())
.withOwner(userBusinessService.getOwnerById(keyResultMetricDto.owner().id()))
.withOwner(userBusinessService.getUserById(keyResultMetricDto.owner().id()))
.withModifiedOn(keyResultMetricDto.modifiedOn()).build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public KeyResult toKeyResultOrdinal(KeyResultOrdinalDto keyResultOrdinalDto) {
.withId(keyResultOrdinalDto.id()).withVersion(keyResultOrdinalDto.version())
.withObjective(objectiveBusinessService.getEntityById(keyResultOrdinalDto.objective().id()))
.withTitle(keyResultOrdinalDto.title()).withDescription(keyResultOrdinalDto.description())
.withOwner(userBusinessService.getOwnerById(keyResultOrdinalDto.owner().id()))
.withOwner(userBusinessService.getUserById(keyResultOrdinalDto.owner().id()))
.withModifiedOn(keyResultOrdinalDto.modifiedOn()).build();
}

Expand Down
12 changes: 1 addition & 11 deletions backend/src/main/java/ch/puzzle/okr/models/UserTeam.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

@Entity
@Table(name = "person_team")
public class UserTeam implements WriteableInterface {
public class UserTeam {

@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator = "sequence_person_team")
Expand Down Expand Up @@ -38,16 +38,6 @@ private UserTeam(Builder builder) {
this.isTeamAdmin = builder.isTeamAdmin;
}

@Override
public boolean isWriteable() {
return false;
}

@Override
public void setWriteable(boolean writeable) {

}

public Long getId() {
return id;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ private void checkUserAuthorization(OkrResponseStatusException exception, Long t
throw exception;
}

private boolean isUserWriteAllowed(Long teamId) {
public boolean isUserWriteAllowed(Long teamId) {
AuthorizationUser authorizationUser = authorizationService.getAuthorizationUser();
if (hasRoleWriteAndReadAll(authorizationUser)) {
return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package ch.puzzle.okr.service.authorization;

import ch.puzzle.okr.models.User;
import ch.puzzle.okr.models.authorization.AuthorizationUser;
import ch.puzzle.okr.models.UserTeam;
import ch.puzzle.okr.service.business.UserBusinessService;
import org.springframework.stereotype.Service;

Expand All @@ -12,14 +12,33 @@ public class UserAuthorizationService {
private final UserBusinessService userBusinessService;
private final AuthorizationService authorizationService;

private final TeamAuthorizationService teamAuthorizationService;

public UserAuthorizationService(UserBusinessService userBusinessService,
AuthorizationService authorizationService) {
AuthorizationService authorizationService, TeamAuthorizationService teamAuthorizationService) {
this.userBusinessService = userBusinessService;
this.authorizationService = authorizationService;
this.teamAuthorizationService = teamAuthorizationService;
}

public List<User> getAllUsers() {
AuthorizationUser authorizationUser = authorizationService.getAuthorizationUser();
return userBusinessService.getAllUsers();
var allUsers = userBusinessService.getAllUsers();
allUsers.forEach(this::setTeamWritableForUser);
return allUsers;
}

private void setTeamWritableForUser(User user) {
user.getUserTeamList().forEach(this::setTeamWritableForUserTeam);
}

private void setTeamWritableForUserTeam(UserTeam userTeam) {
var team = userTeam.getTeam();
team.setWriteable(teamAuthorizationService.isUserWriteAllowed(team.getId()));
}

public User getById(long id) {
var user = userBusinessService.getUserById(id);
setTeamWritableForUser(user);
return user;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public List<User> getAllUsers() {
return userPersistenceService.findAll();
}

public User getOwnerById(Long ownerId) {
public User getUserById(Long ownerId) {
return userPersistenceService.findById(ownerId);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

import static ch.puzzle.okr.TestHelper.defaultTeam;
import static ch.puzzle.okr.TestHelper.defaultUserWithTeams;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
Expand All @@ -22,6 +22,8 @@ public class UserAuthorizationServiceTest {
UserBusinessService userBusinessService;
@Mock
AuthorizationService authorizationService;
@Mock
TeamAuthorizationService teamAuthorizationService;
@InjectMocks
private UserAuthorizationService userAuthorizationService;

Expand All @@ -30,16 +32,31 @@ public class UserAuthorizationServiceTest {

private final AuthorizationUser authorizationUser = new AuthorizationUser(
defaultUserWithTeams(1L, List.of(defaultTeam(adminTeamId)), List.of(defaultTeam(memberTeamId))));
User user = User.Builder.builder().withId(5L).withFirstname("firstname").withLastname("lastname")
.withEmail("[email protected]").build();
User user = defaultUserWithTeams(1L, List.of(defaultTeam(adminTeamId), defaultTeam(memberTeamId)), List.of());
User user2 = defaultUserWithTeams(2L, List.of(), List.of(defaultTeam(adminTeamId), defaultTeam(memberTeamId)));

@Test
void getAllUsersShouldReturnAllUsers() {
List<User> userList = List.of(user, user);
when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser);
List<User> userList = List.of(user, user2);
when(userBusinessService.getAllUsers()).thenReturn(userList);
when(teamAuthorizationService.isUserWriteAllowed(adminTeamId)).thenReturn(true);
when(teamAuthorizationService.isUserWriteAllowed(memberTeamId)).thenReturn(false);

List<User> users = userAuthorizationService.getAllUsers();
assertEquals(userList, users);
}

@Test
void getAllUsers_shouldSetTeamWritableCorrectly() {
List<User> userList = List.of(user, user2);
when(userBusinessService.getAllUsers()).thenReturn(userList);
when(teamAuthorizationService.isUserWriteAllowed(adminTeamId)).thenReturn(true);
when(teamAuthorizationService.isUserWriteAllowed(memberTeamId)).thenReturn(false);

List<User> users = userAuthorizationService.getAllUsers();
assertTrue(users.get(0).getUserTeamList().get(0).getTeam().isWriteable());
assertFalse(users.get(0).getUserTeamList().get(1).getTeam().isWriteable());
assertTrue(users.get(1).getUserTeamList().get(0).getTeam().isWriteable());
assertFalse(users.get(1).getUserTeamList().get(1).getTeam().isWriteable());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ void shouldReturnSingleUserWhenFindingOwnerByValidId() {
.withEmail("[email protected]").build();
Mockito.when(userPersistenceService.findById(any())).thenReturn(owner);

User returnedUser = userBusinessService.getOwnerById(1L);
User returnedUser = userBusinessService.getUserById(1L);

assertEquals(1L, returnedUser.getId());
assertEquals("Bob", returnedUser.getFirstname());
Expand Down
16 changes: 10 additions & 6 deletions frontend/src/app/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ import { User } from '../shared/types/model/User';
export class UserService {
private readonly API_URL = 'api/v1/users';

private _user: User | undefined;
private _currentUser: User | undefined;
private users: BehaviorSubject<User[]> = new BehaviorSubject<User[]>([]);
private usersLoaded = false;

constructor(private httpClient: HttpClient) {}

public initCurrentUser(): Observable<User> {
if (this._user) {
return of(this._user);
if (this._currentUser) {
return of(this._currentUser);
}
return this.httpClient.get<User>(this.API_URL + '/current').pipe(tap((u) => (this._user = u)));
return this.httpClient.get<User>(this.API_URL + '/current').pipe(tap((u) => (this._currentUser = u)));
}

public getUsers(): Observable<User[]> {
Expand All @@ -35,9 +35,13 @@ export class UserService {
}

public getCurrentUser(): User {
if (!this._user) {
if (!this._currentUser) {
throw new Error('user should not be undefined here');
}
return this._user;
return this._currentUser;
}

getUserById(id: number): Observable<User> {
return this.httpClient.get<User>(this.API_URL + '/' + id);
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -1,3 +1,42 @@
<div class="w-100" cdkTrapFocus cdkTrapFocusAutoCapture="true" [attr.data-testId]="'side-panel'">
<p>test</p>
<div *ngIf="user$ | async as user; else spinner">
<div class="w-100">
<h2>
{{ getFullNameFromUser(user) }}
<span *ngIf="selectedUserIsLoggedInUser">(ich)</span>
</h2>
</div>
<div class="w-100">
<table mat-table [dataSource]="user.userTeamList" class="okr-table">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Name</th>
<td mat-cell *matCellDef="let element">{{ element.team.name }}</td>
</ng-container>
<ng-container matColumnDef="role">
<th mat-header-cell *matHeaderCellDef>Rolle</th>
<td mat-cell *matCellDef="let element">
<div class="role">
<span>{{ getRole(element) }}</span>
<button mat-icon-button (click)="editTeamMembership(element, user)" *ngIf="isEditable(element)">
<mat-icon class="d-flex justify-content-center align-items-center">edit</mat-icon>
</button>
</div>
</td>
</ng-container>
<ng-container matColumnDef="delete">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let element">
<button *ngIf="isDeletable(element, user)" mat-icon-button (click)="removeTeamMembership(element, user)">
<mat-icon class="d-flex justify-content-center align-items-center">delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
</div>
</div>
<ng-template #spinner>
<app-spinner text="Member wird geladen..."></app-spinner>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
:host {
width: 100%;
}

.mat-column-delete {
width: 20px;
}

.role {
display: flex;
flex-direction: row;
align-items: center;
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,75 @@
import {Component, OnInit} from '@angular/core';
import { Component, OnInit, ViewChild } from '@angular/core';
import { UserService } from '../../services/user.service';
import { mergeMap, Observable, tap } from 'rxjs';
import { getFullNameFromUser, User } from '../../shared/types/model/User';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Team } from '../../shared/types/model/Team';
import { UserTeam } from '../../shared/types/model/UserTeam';
import { TranslateService } from '@ngx-translate/core';
import { MatTable } from '@angular/material/table';

@Component({
selector: 'app-member-detail',
templateUrl: './member-detail.component.html',
styleUrl: './member-detail.component.css'
styleUrl: './member-detail.component.scss',
})
export class MemberDetailComponent implements OnInit {
@ViewChild(MatTable) table!: MatTable<User[]>;

user$: Observable<User> | undefined;
teams: Team[] = [];
selectedUserIsLoggedInUser: boolean = false;
readonly displayedColumns = ['name', 'role', 'delete'];

readonly getFullNameFromUser = getFullNameFromUser;

constructor(
) {
}
private readonly userService: UserService,
private readonly route: ActivatedRoute,
private readonly translateService: TranslateService,
) {}
ngOnInit(): void {
this.user$ = this.route.paramMap.pipe(
mergeMap((params) => {
const id = this.getIdFromParams(params);
return this.userService.getUserById(id);
}),
tap((user) => this.setSelectedUserIsLoggedinUser(user)),
);
}

private setSelectedUserIsLoggedinUser(selectedUser: User) {
this.selectedUserIsLoggedInUser = selectedUser.id === this.userService.getCurrentUser().id;
}

private getIdFromParams(params: ParamMap): number {
const id = params.get('id');
if (!id) {
throw Error('member id is undefined');
}
return parseInt(id);
}

getRole(userTeam: UserTeam): string {
if (userTeam.isTeamAdmin) {
return this.translateService.instant('USER_ROLE.TEAM_ADMIN');
}
return this.translateService.instant('USER_ROLE.TEAM_MEMBER');
}

removeTeamMembership(userTeam: UserTeam, user: User) {
alert('not implemented');
}

editTeamMembership(userTeam: UserTeam, user: User) {
alert('not implemented');
}

isEditable(userTeam: UserTeam) {
return userTeam.team.isWriteable;
}

isDeletable(userTeam: UserTeam, user: User): boolean {
return this.isEditable(userTeam) || this.selectedUserIsLoggedInUser;
}
}

0 comments on commit 734215e

Please sign in to comment.