feat(project): enforce activation guards
This commit is contained in:
@@ -63,6 +63,12 @@ public class ProjectController {
|
||||
return "projects/detail";
|
||||
}
|
||||
|
||||
@PostMapping("/{projectId}/activate")
|
||||
public String activate(Principal principal, @PathVariable long projectId) {
|
||||
projects.activate(actorId(principal), projectId);
|
||||
return "redirect:/projects/" + projectId;
|
||||
}
|
||||
|
||||
@GetMapping("/{projectId}/members")
|
||||
public String members(Principal principal, @PathVariable long projectId, Model model) {
|
||||
long actorId = actorId(principal);
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@Table(name = "projects")
|
||||
@@ -157,15 +158,23 @@ public class ProjectEntity {
|
||||
this, change.replacement(), change.effectiveAt(), actorMentorUserId));
|
||||
}
|
||||
|
||||
public void activate(long actorMentorUserId, boolean allTaskAssigneesAreCurrent, Instant at) {
|
||||
public void activate(
|
||||
long actorMentorUserId,
|
||||
Set<Long> activeInternUserIds,
|
||||
boolean allTaskAssigneesAreCurrent,
|
||||
Instant at) {
|
||||
requireOwner(actorMentorUserId);
|
||||
Objects.requireNonNull(activeInternUserIds, "activeInternUserIds");
|
||||
Objects.requireNonNull(at, "at");
|
||||
if (status != ProjectStatus.PLANNED) {
|
||||
throw new ProjectRuleViolationException("Only a planned Project can be activated");
|
||||
}
|
||||
if (memberships.stream().noneMatch(ProjectMembershipEntity::isCurrent)
|
||||
|| leadershipTerms.stream().noneMatch(ProjectLeadershipTermEntity::isCurrent)) {
|
||||
throw new ProjectRuleViolationException("Project requires a current member and Leader");
|
||||
if (memberships.stream().noneMatch(membership -> membership.isCurrent()
|
||||
&& activeInternUserIds.contains(membership.internUserId()))) {
|
||||
throw new ProjectRuleViolationException("Project requires an active member");
|
||||
}
|
||||
if (!activeInternUserIds.contains(currentLeader().internUserId())) {
|
||||
throw new ProjectRuleViolationException("Project Leader must be an active member");
|
||||
}
|
||||
if (!allTaskAssigneesAreCurrent) {
|
||||
throw new ProjectRuleViolationException("Every current Task assignee must be an active Project member");
|
||||
|
||||
@@ -7,7 +7,10 @@ import com.lab.labtimesheet.feature.project.model.dto.ProjectCreateCommand;
|
||||
import com.lab.labtimesheet.feature.project.model.dto.ProjectTaskContext;
|
||||
import com.lab.labtimesheet.feature.project.model.entity.ProjectEntity;
|
||||
import com.lab.labtimesheet.feature.project.repository.ProjectRepository;
|
||||
import com.lab.labtimesheet.feature.task.service.TaskQueryService;
|
||||
import java.time.Clock;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -17,16 +20,19 @@ public class ProjectService {
|
||||
private final ProjectRepository projects;
|
||||
private final AccountService accounts;
|
||||
private final ProjectQueryService queries;
|
||||
private final TaskQueryService taskQueries;
|
||||
private final Clock clock;
|
||||
|
||||
public ProjectService(
|
||||
ProjectRepository projects,
|
||||
AccountService accounts,
|
||||
ProjectQueryService queries,
|
||||
TaskQueryService taskQueries,
|
||||
Clock clock) {
|
||||
this.projects = projects;
|
||||
this.accounts = accounts;
|
||||
this.queries = queries;
|
||||
this.taskQueries = taskQueries;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@@ -70,6 +76,27 @@ public class ProjectService {
|
||||
return queries.taskContext(actorUserId, lockedProject(projectId));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void activate(long actorUserId, long projectId) {
|
||||
var project = lockedProject(projectId);
|
||||
project.authorizeOwner(actorUserId);
|
||||
var activeMemberships = project.memberships().stream()
|
||||
.filter(membership -> membership.isCurrent()
|
||||
&& accounts.isEligibleIntern(membership.internUserId()))
|
||||
.toList();
|
||||
var activeMembershipIds = activeMemberships.stream()
|
||||
.map(membership -> membership.id())
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
Set<Long> activeInternUserIds = activeMemberships.stream()
|
||||
.map(membership -> membership.internUserId())
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
var allTaskAssigneesAreCurrent = taskQueries.countCurrentTasksAssignedOutside(
|
||||
projectId, activeMembershipIds) == 0;
|
||||
|
||||
project.activate(actorUserId, activeInternUserIds, allTaskAssigneesAreCurrent, clock.instant());
|
||||
projects.flush();
|
||||
}
|
||||
|
||||
private ProjectEntity lockedProject(long projectId) {
|
||||
return projects.findLockedById(projectId).orElseThrow(ProjectAccessDeniedException::new);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
<h1 th:text="${project.name}">Project</h1>
|
||||
<p th:text="${project.description}"></p>
|
||||
<dl><dt>Status</dt><dd th:text="${project.status}"></dd><dt>Mentor</dt><dd th:text="${project.mentorName}"></dd><dt>Leader</dt><dd th:text="${project.leaderName}"></dd></dl>
|
||||
<form th:if="${project.canManage and project.status == 'PLANNED'}" th:action="@{/projects/{id}/activate(id=${project.id})}" method="post">
|
||||
<button type="submit">Activate</button>
|
||||
</form>
|
||||
<nav aria-label="Project sections"><a th:href="@{/projects/{id}/members(id=${project.id})}">Members</a> <a th:href="@{/projects/{id}/leadership(id=${project.id})}">Leadership</a></nav>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user