Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pl.edu.agh.project_manager.domain.enums.GroupType;
import pl.edu.agh.project_manager.service.command.project.ProjectGroupCreationCommand;

import java.util.List;
import java.util.UUID;

public record ProjectGroupCreationRequest(
Expand All @@ -15,14 +16,17 @@ public record ProjectGroupCreationRequest(
String description,

@NotNull(message = "Typ grupy nie może być pusty")
GroupType groupType
GroupType groupType,

List<@NotNull UUID> projectIds
) {

public ProjectGroupCreationCommand toCommand(UUID ownerId) {
return new ProjectGroupCreationCommand(
this.name,
this.description,
this.groupType,
this.projectIds,
ownerId
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import pl.edu.agh.project_manager.controller.dto.project.*;
import pl.edu.agh.project_manager.domain.enums.GroupType;
import pl.edu.agh.project_manager.security.UserPrincipal;
import pl.edu.agh.project_manager.service.command.project.SearchProjectCommand;
import pl.edu.agh.project_manager.service.project.ProjectService;
import pl.edu.agh.project_manager.service.command.project.ProjectCreationCommand;

Expand Down Expand Up @@ -36,10 +38,18 @@ public ResponseEntity<UUID> createProject(
@GetMapping
@PreAuthorize("hasAnyRole('PROJECT_MANAGER', 'AUTHORITY', 'LINEAR_MANAGER', 'COMMON', 'ADMINISTRATOR')")
public ResponseEntity<List<ProjectResponse>> getAllProjects(
@AuthenticationPrincipal UserPrincipal userPrincipal
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(value = "query", required = false) String query,
@RequestParam(value = "groupId", required = false) UUID groupId,
@RequestParam(value = "unassignedOnly", required = false, defaultValue = "false") Boolean unassignedOnly
Comment thread
mistermotopl marked this conversation as resolved.
) {
List<ProjectResponse> projects = projectService.getAccessibleProjects(userPrincipal);
return ResponseEntity.ok(projects);
SearchProjectCommand command = new SearchProjectCommand(
userPrincipal,
query,
groupId,
unassignedOnly
);
return ResponseEntity.ok(projectService.searchProjects(command));
}

@PreAuthorize("hasAnyAuthority('ADMINISTRATOR', 'AUTHORITY') or @projectAccess.canAccessProject(#projectId, authentication.principal)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,9 @@ public class ProjectGroup {
@OneToMany(mappedBy = "projectGroup")
@Builder.Default
private List<Project> projects = new ArrayList<>();

public void addProject(Project project) {
this.projects.add(project);
project.setProjectGroup(this);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package pl.edu.agh.project_manager.repository.project;

import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
Expand Down Expand Up @@ -41,4 +42,7 @@ public interface ProjectRepository extends JpaRepository<Project, UUID> {
@EntityGraph(attributePaths = {"projectManager", "projectManager.qualifications"})
@Query("SELECT DISTINCT p FROM Project p JOIN p.members m WHERE m.user.id = :userId")
List<Project> findAllByMemberId(@Param("userId") UUID userId);

@EntityGraph(attributePaths = {"projectManager"})
List<Project> findAll(Specification<Project> spec);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import pl.edu.agh.project_manager.domain.enums.GroupType;

import java.util.List;
import java.util.UUID;

public record ProjectGroupCreationCommand(
String name,
String description,
GroupType groupType,
List<UUID> projectIds,
UUID ownerId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package pl.edu.agh.project_manager.service.command.project;

import pl.edu.agh.project_manager.domain.enums.UserRole;
import pl.edu.agh.project_manager.security.UserPrincipal;

import java.util.UUID;

public record SearchProjectCommand(
UserPrincipal user,
String query,
UUID groupId,
Boolean unassignedOnly
) {
public SearchProjectCommand toCommand(UserPrincipal user, String query, UUID groupId, Boolean unassignedOnly) {
return new SearchProjectCommand(user, query, groupId, unassignedOnly);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package pl.edu.agh.project_manager.service.project;

import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.Predicate;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import pl.edu.agh.project_manager.controller.dto.project.ProjectMembersResponse;
import pl.edu.agh.project_manager.controller.dto.project.ProjectResponse;
Expand All @@ -10,6 +13,7 @@
import pl.edu.agh.project_manager.domain.entity.projectgroup.ProjectGroup;
import pl.edu.agh.project_manager.domain.entity.user.User;
import pl.edu.agh.project_manager.domain.entity.project.ProjectMilestone;
import pl.edu.agh.project_manager.domain.enums.GroupType;
import pl.edu.agh.project_manager.domain.exception.ApiErrorCode;
import pl.edu.agh.project_manager.domain.exception.ApplicationException;
import pl.edu.agh.project_manager.repository.project.ProjectRepository;
Expand All @@ -18,9 +22,11 @@
import pl.edu.agh.project_manager.service.command.project.ProjectCreationCommand;
import pl.edu.agh.project_manager.service.command.project.RiskCommand;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import pl.edu.agh.project_manager.domain.enums.UserRole;
import pl.edu.agh.project_manager.service.command.project.SearchProjectCommand;
import pl.edu.agh.project_manager.service.projectgroup.ProjectGroupsService;
import pl.edu.agh.project_manager.service.user.UserService;

Expand Down Expand Up @@ -56,23 +62,6 @@ public UUID createProject(ProjectCreationCommand command) {
return savedProject.getId();
}

@Transactional
public List<ProjectResponse> getAccessibleProjects(UserPrincipal userPrincipal) {

UserRole role = userPrincipal.userRole();

List<Project> projects = switch (role) {
case ADMINISTRATOR, AUTHORITY -> projectRepository.findAll();
case PROJECT_MANAGER -> projectRepository.findAllByProjectManagerId(userPrincipal.userId());
case LINEAR_MANAGER, COMMON -> projectRepository.findAllByMemberId(userPrincipal.userId());
default -> List.of();
};

return projects.stream()
.map(ProjectResponse::from)
.toList();
}

public ProjectResponse getProject(UUID projectId) {
Project project = projectRepository.findByIdWithManager(projectId)
.orElseThrow(() -> new ApplicationException(
Expand Down Expand Up @@ -154,4 +143,30 @@ public Project getProjectEntityOrThrow(UUID projectId) {
.orElseThrow(() -> new ApplicationException(ApiErrorCode.PROJECT_NOT_FOUND, "Cannot find project: " + projectId));
}

@Transactional
public List<ProjectResponse> searchProjects(SearchProjectCommand command) {
Comment thread
mistermotopl marked this conversation as resolved.
Specification<Project> spec = Specification
.where(ProjectSpecification.accessibleByUser(command.user()))
.and(buildSearchFilter(command));

return projectRepository.findAll(spec).stream()
.map(ProjectResponse::from)
.toList();
}

private Specification<Project> buildSearchFilter(SearchProjectCommand command) {
Specification<Project> spec = (root, query, cb) -> cb.conjunction();

if (command.query() != null && !command.query().isBlank()) {
spec = spec.and(ProjectSpecification.withSearchPattern(command.query()));
}

if (command.groupId() != null) {
spec = spec.and(ProjectSpecification.inGroup(command.groupId()));
} else if (Boolean.TRUE.equals(command.unassignedOnly())) {
spec = spec.and(ProjectSpecification.unassigned());
}

return spec;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package pl.edu.agh.project_manager.service.project;

import jakarta.persistence.criteria.Join;
import org.springframework.data.jpa.domain.Specification;
import pl.edu.agh.project_manager.domain.entity.project.Project;
import pl.edu.agh.project_manager.domain.entity.user.User;
import pl.edu.agh.project_manager.security.UserPrincipal;

import java.util.UUID;

public class ProjectSpecification {

public static Specification<Project> withSearchPattern(String query) {
return (root, query1, cb) -> {
String pattern = "%" + query.toLowerCase() + "%";
return cb.like(cb.lower(root.get("title")), pattern);
};
}

public static Specification<Project> accessibleByUser(UserPrincipal user) {
return (root, query, cb) -> switch (user.userRole()) {
case PROJECT_MANAGER ->
cb.equal(root.get("projectManager").get("id"), user.userId());
case LINEAR_MANAGER, COMMON -> {
Join<Project, User> membersJoin = root.join("members");
query.distinct(true);
yield cb.equal(membersJoin.get("id"), user.userId());
}
case ADMINISTRATOR, AUTHORITY -> cb.conjunction();
};
}

public static Specification<Project> inGroup(UUID groupId) {
return (root, query, cb) -> cb.equal(root.get("projectGroup").get("id"), groupId);
}

public static Specification<Project> unassigned() {
return (root, query, cb) -> cb.isNull(root.get("projectGroup"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@

import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import pl.edu.agh.project_manager.controller.dto.project_group.AllGroupsResponse;
import pl.edu.agh.project_manager.controller.dto.project_group.GroupOwnerResponse;
import pl.edu.agh.project_manager.controller.dto.project_group.SingleGroupDetailsResponse;
import pl.edu.agh.project_manager.controller.dto.project_group.SingleGroupResponse;
import pl.edu.agh.project_manager.domain.entity.project.Project;
import pl.edu.agh.project_manager.domain.entity.projectgroup.ProjectGroup;
import pl.edu.agh.project_manager.domain.entity.user.User;
import pl.edu.agh.project_manager.domain.enums.GroupType;
import pl.edu.agh.project_manager.domain.exception.ApiErrorCode;
import pl.edu.agh.project_manager.domain.exception.ApplicationException;
import pl.edu.agh.project_manager.repository.project.ProjectRepository;
import pl.edu.agh.project_manager.repository.projectgroup.ProjectGroupRepository;
import pl.edu.agh.project_manager.repository.user.UserRepository;
import pl.edu.agh.project_manager.service.command.project.ProjectGroupCreationCommand;
Expand All @@ -24,6 +27,7 @@ public class ProjectGroupsService {

private final ProjectGroupRepository projectGroupRepository;
private final UserRepository userRepository;
private final ProjectRepository projectRepository;

public AllGroupsResponse getAllGroups() {
return new AllGroupsResponse(getWalletGroups(), getProgramGroups());
Expand Down Expand Up @@ -56,13 +60,15 @@ public SingleGroupDetailsResponse getGroupById(UUID id) {
);
}

@Transactional
public UUID createGroup(ProjectGroupCreationCommand command) {
User owner = userRepository.findById(command.ownerId())
.orElseThrow(() -> new ApplicationException(ApiErrorCode.USER_NOT_FOUND, "Cannot find user with id: " + command.ownerId()));

ProjectGroup projectGroup = buildProjectGroups(command, owner);

ProjectGroup savedGroup = projectGroupRepository.save(projectGroup);
addProjectToProjectGroup(command.projectIds(), projectGroup);

return savedGroup.getId();
}
Expand All @@ -87,4 +93,14 @@ public ProjectGroup getProjectGroupOrThrow(UUID groupId) {
return projectGroupRepository.findById(groupId)
.orElseThrow(() -> new ApplicationException(ApiErrorCode.PROJECT_GROUP_NOT_FOUND, "Cannot find group: " + groupId));
}

private void addProjectToProjectGroup(List<UUID> projectIds, ProjectGroup projectGroups) {
List<Project> projects = projectRepository.findAllById(projectIds);

if (projects.size() != projectIds.size()) {
throw new ApplicationException(ApiErrorCode.PROJECT_NOT_FOUND, "Cannot find all projects with provided ids");
}

projects.forEach(projectGroups::addProject);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.jpa.domain.Specification;
import pl.edu.agh.project_manager.domain.entity.project.Project;
import pl.edu.agh.project_manager.domain.entity.user.User;
import pl.edu.agh.project_manager.domain.enums.UserRole;
import pl.edu.agh.project_manager.domain.exception.ApiErrorCode;
import pl.edu.agh.project_manager.domain.exception.ApplicationException;
import pl.edu.agh.project_manager.repository.project.ProjectRepository;
import pl.edu.agh.project_manager.security.UserPrincipal;
import pl.edu.agh.project_manager.service.command.project.SearchProjectCommand;
import pl.edu.agh.project_manager.service.projectgroup.ProjectGroupsService;
import pl.edu.agh.project_manager.service.user.UserService;
import pl.edu.agh.project_manager.service.command.project.MilestoneCommand;
Expand Down Expand Up @@ -99,17 +101,17 @@ void getAccessibleProjects_Administrator() {
// Given
UUID adminId = UUID.randomUUID();
UserPrincipal principal = createPrincipal(adminId, UserRole.ADMINISTRATOR);
SearchProjectCommand searchCommand = new SearchProjectCommand(principal, null, null, false);

when(projectRepository.findAll()).thenReturn(List.of(
when(projectRepository.findAll(any(Specification.class))).thenReturn(List.of(
createTestProject(UUID.randomUUID(), "Projekt A"),
createTestProject(UUID.randomUUID(), "Projekt B")
));
// When
var result = projectService.getAccessibleProjects(principal);
var result = projectService.searchProjects(searchCommand);

// Then
assertThat(result).hasSize(2);
verify(projectRepository).findAll();
}

@Test
Expand All @@ -118,17 +120,17 @@ void getAccessibleProjects_ProjectManager() {
// Given
UUID pmId = UUID.randomUUID();
UserPrincipal principal = createPrincipal(pmId, UserRole.PROJECT_MANAGER);
SearchProjectCommand searchCommand = new SearchProjectCommand(principal, null, null, false);

when(projectRepository.findAllByProjectManagerId(pmId)).thenReturn(List.of(
when(projectRepository.findAll(any(Specification.class))).thenReturn(List.of(
createTestProject(UUID.randomUUID(), "Projekt A")
));

// When
var result = projectService.getAccessibleProjects(principal);
var result = projectService.searchProjects(searchCommand);

// Then
assertThat(result).hasSize(1);
verify(projectRepository).findAllByProjectManagerId(pmId);
}

@Test
Expand All @@ -137,17 +139,17 @@ void getAccessibleProjects_CommonUser() {
// Given
UUID userId = UUID.randomUUID();
UserPrincipal principal = createPrincipal(userId, UserRole.COMMON);
SearchProjectCommand searchCommand = new SearchProjectCommand(principal, null, null, false);

when(projectRepository.findAllByMemberId(userId)).thenReturn(List.of(
when(projectRepository.findAll(any(Specification.class))).thenReturn(List.of(
createTestProject(UUID.randomUUID(), "Projekt A")
));

// When
var result = projectService.getAccessibleProjects(principal);
var result = projectService.searchProjects(searchCommand);

// Then
assertThat(result).hasSize(1);
verify(projectRepository).findAllByMemberId(userId);
}

@Test
Expand All @@ -172,7 +174,6 @@ void getProjectMembers_Success() {
// Then
assertThat(response).isNotNull();
assertThat(response.sponsors()).hasSize(1);
verify(projectRepository).findByIdWithAllMembers(projectId);
}

private ProjectCreationCommand createBasicCommand(UUID managerId) {
Expand Down
Loading
Loading