Split Project #146

Open
tozh4728 wants to merge 38 commits from 87-split-project into develop
28 changed files with 1113 additions and 41 deletions

@ -145,6 +145,7 @@ import se.su.dsv.scipro.project.ProjectPeopleStatisticsServiceImpl;
import se.su.dsv.scipro.project.ProjectRepo;
import se.su.dsv.scipro.project.ProjectService;
import se.su.dsv.scipro.project.ProjectServiceImpl;
import se.su.dsv.scipro.project.split.SplitOrRestartProjectServiceImpl;
import se.su.dsv.scipro.projectpartner.ProjectPartnerServiceImpl;
import se.su.dsv.scipro.reflection.ReflectionService;
import se.su.dsv.scipro.reflection.ReflectionServiceImpl;
@ -814,6 +815,21 @@ public class CoreConfig {
return new ProjectServiceImpl(projectRepo, clock, eventBus, em);
}
@Bean
public SplitOrRestartProjectServiceImpl SplitOrRestartProjectService(
ProjectService projectService,
FinalSeminarService finalSeminarService,
RoughDraftApprovalService roughDraftApprovalService,
EventBus eventBus
) {
return new SplitOrRestartProjectServiceImpl(
projectService,
finalSeminarService,
roughDraftApprovalService,
eventBus
);
}
@Bean
public ProjectTypeServiceImpl projectTypeService(Provider<EntityManager> em) {
return new ProjectTypeServiceImpl(em);

@ -21,6 +21,7 @@ import se.su.dsv.scipro.peer.SecondPeerReviewCompletedEvent;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.report.SupervisorGradingReportSubmittedEvent;
import se.su.dsv.scipro.reviewing.FinalSeminarApprovalApprovedEvent;
import se.su.dsv.scipro.reviewing.RoughDraftApprovalApprovedClonedEvent;
import se.su.dsv.scipro.reviewing.RoughDraftApprovalApprovedEvent;
import se.su.dsv.scipro.reviewing.RoughDraftApprovalRequestedEvent;
import se.su.dsv.scipro.system.User;
@ -136,6 +137,11 @@ public class MilestoneActivator {
activateProjectMilestone(Set.of(event.getName()), event.getProject());
}
@Subscribe
public void reviewerApprovalApprovedClone(RoughDraftApprovalApprovedClonedEvent event) {
activateProjectMilestone(Set.of(event.getName()), event.getProject());
}
@Subscribe
public void finalSeminarThesisDeleted(FinalSeminarThesisDeletedEvent event) {
deactivateProjectMilestone(Set.of("FinalSeminarThesisUploaded"), event.getFinalSeminar().getProject());

@ -23,6 +23,7 @@ import jakarta.persistence.MapKeyJoinColumn;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
@ -113,6 +114,18 @@ public class Project extends DomainObject {
@Column(name = "daisy_identifier", unique = true)
private Integer identifier;
@Basic
@Column(name = "parent_project_id")
private Long parentProjectId;
@Basic
@Column(name = "root_project_id")
private Long rootProjectId;
@Basic
@Column(name = "clone_timestamp")
private Instant cloneTimestamp;
// ----------------------------------------------------------------------------------
tozh4728 marked this conversation as resolved Outdated

This should be a java.time.Instant not java.util.Date.

@Temporal is deprecated.

This should be a `java.time.Instant` not `java.util.Date`. [`@Temporal` is deprecated](https://jakarta.ee/specifications/persistence/3.2/apidocs/jakarta.persistence/jakarta/persistence/temporal).
// Embedded JPA-mapping
// ----------------------------------------------------------------------------------
@ -365,6 +378,30 @@ public class Project extends DomainObject {
this.userNotes = userNotes;
}
public Long getParentProjectId() {
return parentProjectId;
}
public void setParentProjectId(Long parentProjectId) {
this.parentProjectId = parentProjectId;
}
public Long getRootProjectId() {
return rootProjectId;
}
public void setRootProjectId(Long rootProjectId) {
this.rootProjectId = rootProjectId;
}
public Instant getCloneTimestamp() {
return cloneTimestamp;
}
public void setCloneTimestamp(Instant cloneTimestamp) {
this.cloneTimestamp = cloneTimestamp;
}
// ----------------------------------------------------------------------------------
// Methods Common To All Objects
// ----------------------------------------------------------------------------------

@ -0,0 +1,28 @@
package se.su.dsv.scipro.project.split;
import java.util.List;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.reviewing.RoughDraftApproval;
public interface SplitOrRestartProjectService {
enum SplittableStatus {
NOT_EXIST,
NOT_ACTIVE,
NOT_TWO_PARTICIPANTS,
PHASE_TWO_STARTED,
FINAL_SEMINAR_PHASE_STARTED,
OK,
}
record SplittableStatusRecord(
SplittableStatus splittableStatus,
Project project,
RoughDraftApproval roughDraftApproval
) {}
SplittableStatusRecord getSplittableStatus(long projectId);
void splitProject(long projectId);
List<Project> getChildProjects(long parentProjectId);
}

@ -0,0 +1,127 @@
package se.su.dsv.scipro.project.split;
import com.google.common.eventbus.EventBus;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import se.su.dsv.scipro.finalseminar.FinalSeminarService;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.project.ProjectService;
import se.su.dsv.scipro.project.ProjectStatus;
import se.su.dsv.scipro.project.QProject;
import se.su.dsv.scipro.reviewing.RoughDraftApproval;
import se.su.dsv.scipro.reviewing.RoughDraftApprovalApprovedClonedEvent;
import se.su.dsv.scipro.reviewing.RoughDraftApprovalService;
import se.su.dsv.scipro.system.User;
public class SplitOrRestartProjectServiceImpl implements SplitOrRestartProjectService {
private final ProjectService projectService;
private final FinalSeminarService finalSeminarService;
private final RoughDraftApprovalService roughDraftApprovalService;
private final EventBus eventBus;
@Inject
public SplitOrRestartProjectServiceImpl(
ProjectService projectService,
FinalSeminarService finalSeminarService,
RoughDraftApprovalService roughDraftApprovalService,
EventBus eventBus
) {
this.projectService = projectService;
this.finalSeminarService = finalSeminarService;
this.roughDraftApprovalService = roughDraftApprovalService;
this.eventBus = eventBus;
}
@Override
@Transactional
public SplittableStatusRecord getSplittableStatus(long projectId) {
Project project = projectService.findOne(projectId);
if (project == null) return new SplittableStatusRecord(SplittableStatus.NOT_EXIST, null, null);
if (project.getProjectStatus() != ProjectStatus.ACTIVE) {
return new SplittableStatusRecord(SplittableStatus.NOT_ACTIVE, project, null);
}
if (project.getProjectParticipants().size() != 2) {
return new SplittableStatusRecord(SplittableStatus.NOT_TWO_PARTICIPANTS, project, null);
}
if (finalSeminarService.findByProject(project) != null) {
return new SplittableStatusRecord(SplittableStatus.FINAL_SEMINAR_PHASE_STARTED, project, null);
}
Optional<RoughDraftApproval> o = roughDraftApprovalService.findBy(project);
if (o.isPresent() && !o.get().isDecided()) {
return new SplittableStatusRecord(SplittableStatus.PHASE_TWO_STARTED, project, null);
}
if (o.isPresent() && o.get().isApproved()) {
return new SplittableStatusRecord(SplittableStatus.OK, project, o.get());
} else {
return new SplittableStatusRecord(SplittableStatus.OK, project, null);
}
}
@Override
@Transactional
public void splitProject(long projectId) {
SplittableStatusRecord result = getSplittableStatus(projectId);
if (result.splittableStatus() != SplittableStatus.OK) {
throw new IllegalStateException(
"Project must to be verified to be able to split " + "before this method can be called."
);
}
Project project = result.project();
for (User author : project.getProjectParticipants()) {
Project childProject = new Project();
childProject.setTitle(project.getTitle());
childProject.setProjectType(project.getProjectType());
childProject.setProjectStatus(ProjectStatus.ACTIVE);
childProject.setResearchArea(project.getResearchArea());
childProject.setStartDate(project.getStartDate());
childProject.setExpectedEndDate(project.getExpectedEndDate());
childProject.setHeadSupervisor(project.getHeadSupervisor());
childProject.setProjectParticipants(List.of(author));
childProject.setCoSupervisors(project.getCoSupervisors());
childProject.setReviewers(project.getReviewers());
childProject.setParentProjectId(project.getId());
childProject.setRootProjectId(
project.getRootProjectId() != null ? project.getRootProjectId() : project.getId()
);
childProject.setCloneTimestamp(Instant.now());
childProject = projectService.save(childProject);
// Add cloned RoughDraftApproval if it's 'APPROVED'
RoughDraftApproval rda = result.roughDraftApproval();
if (rda != null && rda.isApproved()) {
RoughDraftApproval clonedRda = roughDraftApprovalService.saveCloned(rda.cloneToProject(childProject));
// Send event to eventBus to synchronize eventual Phase Two Approval with MileStone
eventBus.post(new RoughDraftApprovalApprovedClonedEvent(clonedRda));
}
}
// Parent project will set as inactive
project.setProjectStatus(ProjectStatus.INACTIVE);
projectService.save(project);
}
@Override
@Transactional
public List<Project> getChildProjects(long parentProjectId) {
return projectService.findAll(QProject.project.rootProjectId.eq(parentProjectId));
}
}

@ -196,6 +196,25 @@ public class Decision {
decideWithDecisionDate(status, reason, attachment, Instant.now());
}
Decision cloneToReviewerApproval(RoughDraftApproval roughDraftApproval) {
Decision clonedDecision = new Decision();
clonedDecision.reviewerApproval = roughDraftApproval;
clonedDecision.status = this.status;
clonedDecision.reason = this.reason;
clonedDecision.comment = this.comment;
clonedDecision.requested = this.requested;
clonedDecision.decisionDate = this.decisionDate;
clonedDecision.deadline = this.deadline;
clonedDecision.reviewerAssignedAt = this.reviewerAssignedAt;
clonedDecision.assignedReviewer = this.assignedReviewer;
clonedDecision.attachment = this.attachment;
clonedDecision.thesis = this.thesis;
return clonedDecision;
}
private void decideWithDecisionDate(
final Status status,
final String reason,

@ -25,6 +25,7 @@ public class DecisionRepositoryImpl extends AbstractRepository implements Decisi
.eq(reviewer)
.and(QDecision.decision.reviewerAssignedAt.goe(fromDate))
.and(QDecision.decision.reviewerAssignedAt.loe(toDate))
.and(QDecision.decision.reviewerApproval.isCloned.isFalse())
)
.distinct()
.fetchCount();

@ -1,6 +1,8 @@
package se.su.dsv.scipro.reviewing;
import jakarta.persistence.Basic;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.DiscriminatorColumn;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
@ -11,6 +13,7 @@ import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.OrderBy;
import jakarta.persistence.Table;
import java.time.Instant;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
@ -32,6 +35,14 @@ public abstract class ReviewerApproval extends DomainObject {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Basic
@Column(name = "is_cloned")
protected Boolean isCloned = false;
@Basic
@Column(name = "clone_timestamp")
protected Instant cloneTimestamp;
// ----------------------------------------------------------------------------------
tozh4728 marked this conversation as resolved Outdated

This should be a java.time.Instant not java.util.Date.

@Temporal is deprecated.

This should be a `java.time.Instant` not `java.util.Date`. [`@Temporal` is deprecated](https://jakarta.ee/specifications/persistence/3.2/apidocs/jakarta.persistence/jakarta/persistence/temporal).
// JPA-mappings of foreign keys in this table (reviewer_approval) referencing other
// tables.
@ -59,6 +70,22 @@ public abstract class ReviewerApproval extends DomainObject {
return this.project;
}
public Boolean getCloned() {
return isCloned;
}
public void setCloned(Boolean cloned) {
isCloned = cloned;
}
public Instant getCloneTimestamp() {
return cloneTimestamp;
}
public void setCloneTimestamp(Instant cloneDate) {
this.cloneTimestamp = cloneDate;
}
// ----------------------------------------------------------------------------------
// Other methods
// ----------------------------------------------------------------------------------

@ -1,7 +1,8 @@
package se.su.dsv.scipro.reviewing;
import jakarta.persistence.Entity;
import java.util.*;
import java.time.Instant;
import java.util.Date;
import se.su.dsv.scipro.file.FileReference;
import se.su.dsv.scipro.project.Project;
@ -24,4 +25,15 @@ public class RoughDraftApproval extends ReviewerApproval {
public Step getStep() {
return Step.ROUGH_DRAFT_APPROVAL;
}
public RoughDraftApproval cloneToProject(final Project newProject) {
RoughDraftApproval rda = new RoughDraftApproval();
rda.project = newProject;
this.decisions.forEach(decision -> rda.decisions.add(decision.cloneToReviewerApproval(rda)));
rda.isCloned = true;
rda.cloneTimestamp = Instant.now();

If possible try to get the current timestamp from an injected java.time.{Clock/InstantSource}. This makes testing easier since it is then possible to manipulate time (the infrastructure is already in place). Hopefully in the future this time manipulation is available during test data creation as well and maybe even test servers.

If possible try to get the current timestamp from an injected `java.time.{Clock/InstantSource}`. This makes testing easier since it is then possible to manipulate time (the infrastructure is already in place). Hopefully in the future this time manipulation is available during test data creation as well and maybe even test servers.
return rda;
}
}

@ -0,0 +1,14 @@
package se.su.dsv.scipro.reviewing;
import se.su.dsv.scipro.project.Project;
public record RoughDraftApprovalApprovedClonedEvent(ReviewerApproval process) {
public Project getProject() {
return process.getProject();
}
public String getName() {
ReviewerApproval.Step step = process.getStep();
return step.getDeclaringClass().getSimpleName() + "." + step.name();
}
}

@ -1,3 +1,5 @@
package se.su.dsv.scipro.reviewing;
public interface RoughDraftApprovalService extends ReviewerApprovalService<RoughDraftApproval> {}
public interface RoughDraftApprovalService extends ReviewerApprovalService<RoughDraftApproval> {
RoughDraftApproval saveCloned(RoughDraftApproval approval);
}

@ -71,4 +71,10 @@ public class RoughDraftApprovalServiceImpl
public Optional<RoughDraftApproval> findBy(Project project) {
return Optional.ofNullable(findOne(QRoughDraftApproval.roughDraftApproval.project.eq(project)));
}
@Override
@Transactional
public RoughDraftApproval saveCloned(RoughDraftApproval approval) {
return save(approval);
}
}

@ -0,0 +1,28 @@
alter table `project`
add column `parent_project_id` bigint(20) null default null;
alter table `project`
add column `root_project_id` bigint(20) null default null;
alter table `project`
add column `clone_timestamp` datetime null default null;
alter table `project`
add constraint fk_project_parent_project_id_project_id
foreign key (parent_project_id) references project(id)
on delete cascade on update cascade;
alter table `project`
add constraint fk_project_root_project_id_project_id
foreign key (root_project_id) references project(id)
on delete cascade on update cascade;
alter table `reviewer_approval`
add column `is_cloned` boolean not null default false;
update `reviewer_approval` set is_cloned = false;
alter table `reviewer_approval`
add column `clone_timestamp` datetime null default null;

@ -0,0 +1,274 @@
package se.su.dsv.scipro.project.split;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static se.su.dsv.scipro.project.split.SplitOrRestartProjectService.SplittableStatus;
import static se.su.dsv.scipro.project.split.SplitOrRestartProjectService.SplittableStatusRecord;
import jakarta.inject.Inject;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.Year;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.function.Function;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import se.su.dsv.scipro.file.FileUpload;
import se.su.dsv.scipro.finalseminar.FinalSeminar;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.project.ProjectStatus;
import se.su.dsv.scipro.reviewing.*;
import se.su.dsv.scipro.security.auth.roles.Roles;
import se.su.dsv.scipro.system.*;
import se.su.dsv.scipro.test.IntegrationTest;
public class SplitOrRestartProjectServiceIntegrationTest extends IntegrationTest {
private static int TARGET = 3;
private static int REMAINING_TARGET = 2;
@Inject
private SplitOrRestartProjectService splitOrRestartProjectService;
@Inject
private RoughDraftApprovalService roughDraftApprovalService;
@Inject
private FinalSeminarApprovalService finalSeminarApprovalService;
@Inject
private ReviewerAssignmentService reviewerAssignmentService;
@Inject
private ReviewerCapacityService reviewerCapacityService;
private ResearchArea researchArea;
private Project parentProject;
private User supervisor;
private User reviewer;
private User author1;
private User author2;
@BeforeEach
public void setUp() {
ProjectType bachelor = save(new ProjectType(DegreeType.BACHELOR, "Bachelor", "Bachelor"));
researchArea = new ResearchArea();
researchArea.setTitle("Computer Science");
researchArea = save(researchArea);
parentProject = createProject(bachelor, ProjectStatus.ACTIVE);
}
@Test
public void project_must_exist() {
SplittableStatusRecord record = splitOrRestartProjectService.getSplittableStatus(0);
SplittableStatus status = record.splittableStatus();
tozh4728 marked this conversation as resolved
Review

Use assertEquals instead of assertTrue. (This goes for every test).

Use `assertEquals` instead of `assertTrue`. (This goes for every test).
assertEquals(SplittableStatus.NOT_EXIST, status);
}
@Test
public void project_must_be_active() {
parentProject.setProjectStatus(ProjectStatus.INACTIVE);
parentProject = save(parentProject);
SplittableStatusRecord record = splitOrRestartProjectService.getSplittableStatus(parentProject.getId());
SplittableStatus status = record.splittableStatus();
assertEquals(SplittableStatus.NOT_ACTIVE, status);
}
@Test
public void project_must_have_two_participants() {
SplittableStatusRecord record = splitOrRestartProjectService.getSplittableStatus(parentProject.getId());
SplittableStatus status = record.splittableStatus();
assertEquals(SplittableStatus.NOT_TWO_PARTICIPANTS, status);
}
@Test
public void project_phase_two_started() {
setUpBeforePhaseTwo();
SplittableStatusRecord record = splitOrRestartProjectService.getSplittableStatus(parentProject.getId());
SplittableStatus status = record.splittableStatus();
assertEquals(SplittableStatus.PHASE_TWO_STARTED, status);
}
@Test
public void split_on_failed_phase_two() {
setUpBeforePhaseTwo();
Optional<RoughDraftApproval> optional = this.roughDraftApprovalService.findBy(parentProject);
optional.ifPresent(rda -> rda.reject("Fail", Optional.empty()));
SplittableStatusRecord record = splitOrRestartProjectService.getSplittableStatus(parentProject.getId());
SplittableStatus status = record.splittableStatus();
assertEquals(SplittableStatus.OK, status);
splitOrRestartProjectService.splitProject(parentProject.getId());
ReviewerCapacityService.RemainingTargets remainingTargets = reviewerCapacityService.getRemainingTargets(
reviewer,
Year.now()
);
Review

Integration tests have access to time manipulation via injecting an instance of MutableFixedClock. This should allow you to set a specific date in either autumn or spring to get better assertions. Use Year.now(clock) to get the correct year for the fixed date that's been set.

Integration tests have access to time manipulation via injecting an instance of `MutableFixedClock`. This should allow you to set a specific date in either autumn or spring to get better assertions. Use `Year.now(clock)` to get the correct year for the fixed date that's been set.
// Todo: improve this with higher precision by using MutabledFixedClock
assertTrue(remainingTargets.spring() == REMAINING_TARGET || remainingTargets.autumn() == REMAINING_TARGET);
List<Project> childProjects = splitOrRestartProjectService.getChildProjects(parentProject.getId());
assertEquals(ProjectStatus.INACTIVE, parentProject.getProjectStatus());
assertEquals(2, childProjects.size());
childProjects.forEach(project -> {
assertEquals(1, project.getProjectParticipants().size());
assertEquals(supervisor, project.getHeadSupervisor());
assertEquals(ProjectStatus.ACTIVE, project.getProjectStatus());
assertTrue(roughDraftApprovalService.findBy(project).isEmpty());
});
}
@Test
public void split_on_approved_phase_two() {
setUpBeforePhaseTwo();
Optional<RoughDraftApproval> optional = this.roughDraftApprovalService.findBy(parentProject);
optional.ifPresent(rda -> rda.approve("Approve", Optional.empty()));
SplittableStatusRecord record = splitOrRestartProjectService.getSplittableStatus(parentProject.getId());
SplittableStatus status = record.splittableStatus();
assertEquals(SplittableStatus.OK, status);
splitOrRestartProjectService.splitProject(parentProject.getId());
ReviewerCapacityService.RemainingTargets remainingTargets = reviewerCapacityService.getRemainingTargets(
reviewer,
Year.now()
Review

Integration tests have access to time manipulation via injecting an instance of MutableFixedClock. This should allow you to set a specific date in either autumn or spring to get better assertions. Use Year.now(clock) to get the correct year for the fixed date that's been set.

Integration tests have access to time manipulation via injecting an instance of `MutableFixedClock`. This should allow you to set a specific date in either autumn or spring to get better assertions. Use `Year.now(clock)` to get the correct year for the fixed date that's been set.
);
// todo: improve this by using MutableFixedClock
assertTrue(remainingTargets.spring() == REMAINING_TARGET || remainingTargets.autumn() == REMAINING_TARGET);
List<Project> childProjects = splitOrRestartProjectService.getChildProjects(parentProject.getId());
assertEquals(ProjectStatus.INACTIVE, parentProject.getProjectStatus());
assertEquals(2, childProjects.size());
childProjects.forEach(project -> {
assertEquals(1, project.getProjectParticipants().size());
assertEquals(supervisor, project.getHeadSupervisor());
assertEquals(ProjectStatus.ACTIVE, project.getProjectStatus());
Optional<RoughDraftApproval> optionalRda = roughDraftApprovalService.findBy(project);
assertTrue(optionalRda.isPresent());
assertTrue(optional.get().isApproved());
});
}
@Test
public void split_on_final_seminar_phase_started() {
setUpBeforePhaseTwo();
Optional<RoughDraftApproval> optional = this.roughDraftApprovalService.findBy(parentProject);
optional.ifPresent(rda -> rda.approve("Approve", Optional.empty()));
FinalSeminar finalSeminar = new FinalSeminar();
finalSeminar.setProject(parentProject);
finalSeminar.setStartDate(Date.from(ZonedDateTime.now().plusDays(10).toInstant()));
finalSeminar.setRoom("room");
finalSeminar.setPresentationLanguage(Language.ENGLISH);
parentProject.setLanguage(Language.ENGLISH);
save(finalSeminar);
SplittableStatusRecord record = splitOrRestartProjectService.getSplittableStatus(parentProject.getId());
SplittableStatus status = record.splittableStatus();
assertEquals(SplittableStatus.FINAL_SEMINAR_PHASE_STARTED, status);
}
private Project createProject(ProjectType projectType, ProjectStatus active) {
supervisor = createEmployee("Eric", "Employee", "eric@example.com", false);
Project project = new Project();
project.setTitle("Some title");
project.setProjectType(projectType);
project.setProjectStatus(active);
project.setHeadSupervisor(supervisor);
project.setStartDate(LocalDate.now());
author1 = createUser("Adam", "Student", "adam.student@example.com");
project.addProjectParticipant(author1);
project = save(project);
return project;
}
private User createEmployee(String firstName, String lastName, String email, boolean isReviewer) {
User employee = createUser(firstName, lastName, email);
Set<ResearchArea> researchAreas = new HashSet<>();
researchAreas.add(researchArea);
employee.setResearchAreas(researchAreas);
if (isReviewer) {
employee.addRole(Roles.REVIEWER);
}
save(employee);
return employee;
}
private User createUser(String firstName, String lastName, String email) {
User user = User.builder().firstName(firstName).lastName(lastName).emailAddress(email).build();
return save(user);
}
private void setUpBeforePhaseTwo() {
author2 = createUser("Bertil", "Student", "bertil.student@example.com");
parentProject.addProjectParticipant(author2);
save(parentProject);
roughDraftApprovalService.requestApproval(parentProject, dummyFile(), "comment");
reviewer = createEmployee("Lisa", "Employee", "lisa.employee@example.com", true);
reviewerCapacityService.assignTarget(
reviewer,
new ReviewerCapacityService.Target(Year.now(), TARGET, TARGET, "")
);
reviewerAssignmentService.assignReviewer(parentProject, reviewer);
}
private FileUpload dummyFile() {
return new FileUpload() {
@Override
public String getFileName() {
return "dummy.tmp";
}
@Override
public String getContentType() {
return "text/plain";
}
@Override
public User getUploader() {
return null;
}
@Override
public long getSize() {
return 0;
}
@Override
public <T> T handleData(Function<InputStream, T> handler) {
return handler.apply(InputStream.nullInputStream());
}
};
}
}

@ -73,6 +73,9 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
@Inject
private MilestoneActivityTemplateService milestoneActivityTemplateService;
@Inject
private EventService eventService;
@Inject
private FileService fileService;
@ -2016,34 +2019,37 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
MilestoneActivityTemplate.Type.STUDENT,
"First meeting held",
"First meeting with supervisor.",
milestonePhaseTemplate1,
null
);
createMileStone(
MilestoneActivityTemplate.Type.PROJECT,
"Project plan approved",
"Project plan approved by supervisor.",
milestonePhaseTemplate1,
null
milestonePhaseTemplate1
);
createMileStone(
MilestoneActivityTemplate.Type.PROJECT,
"Rough draft sent to reviewer for approval",
"Rough draft approved by reviewer.",
milestonePhaseTemplate2,
null
"Project plan approved",
"Project plan approved by supervisor.",
milestonePhaseTemplate1
);
List<Event> events = eventService.findAll();
createMileStone(
MilestoneActivityTemplate.Type.PROJECT,
"Rough draft approved by reviewer",
"Rough draft sent to reviewer for approval (Auto)",
"Rough draft sent to the reviewer for the first time.",
milestonePhaseTemplate2,
events.stream().filter(event -> event.getName().equals("RoughDraftApprovalRequested")).findFirst().get()
);
createMileStone(
MilestoneActivityTemplate.Type.PROJECT,
"Rough draft approved by reviewer (Auto)",
"Rough draft approved.",
milestonePhaseTemplate2,
null
events.stream().filter(event -> event.getName().equals("Step.ROUGH_DRAFT_APPROVAL")).findFirst().get()
);
createMileStone(
MilestoneActivityTemplate.Type.STUDENT,
"Peer review 1",
"Peer review 1 (Auto)",
"This is a recommendation of when to perform peer review 1.",
milestonePhaseTemplate2,
MilestoneActivityTemplate.PEER_REVIEW_ONE
@ -2053,9 +2059,9 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
MilestoneActivityTemplate.Type.PROJECT,
"Result and discussion completed and approved",
"Result and discussion.",
milestonePhaseTemplate3,
null
milestonePhaseTemplate3
);
createMileStone(
MilestoneActivityTemplate.Type.STUDENT,
"Peer review 2",
@ -2068,9 +2074,9 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
MilestoneActivityTemplate.Type.PROJECT,
"Thesis approved for final seminar presentation",
"Thesis approved for final seminar.",
milestonePhaseTemplate4,
null
milestonePhaseTemplate4
);
createMileStone(
MilestoneActivityTemplate.Type.PROJECT,
"Final seminar created",
@ -2078,6 +2084,7 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
milestonePhaseTemplate4,
MilestoneActivityTemplate.CREATE_SEMINAR
);
createMileStone(
MilestoneActivityTemplate.Type.PROJECT,
"Final seminar thesis uploaded",
@ -2085,64 +2092,93 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
milestonePhaseTemplate4,
MilestoneActivityTemplate.THESIS_UPLOADED
);
createMileStone(
MilestoneActivityTemplate.Type.STUDENT,
"Perform an oral and written opposition",
"Opposition.",
milestonePhaseTemplate4,
null
milestonePhaseTemplate4
);
createMileStone(
MilestoneActivityTemplate.Type.STUDENT,
"Active participation in a final seminar",
"Active participation.",
milestonePhaseTemplate4,
null
milestonePhaseTemplate4
);
createMileStone(
MilestoneActivityTemplate.Type.STUDENT,
"Defend the thesis in a final seminar",
"Defence of final thesis.",
milestonePhaseTemplate4,
null
milestonePhaseTemplate4
);
createMileStone(
MilestoneActivityTemplate.Type.PROJECT,
"Revised final thesis of the submitted thesis",
"Revised final thesis.",
milestonePhaseTemplate5,
null
milestonePhaseTemplate5
);
createMileStone(
MilestoneActivityTemplate.Type.PROJECT,
"Originality report approved",
"Originality report.",
milestonePhaseTemplate5,
null
milestonePhaseTemplate5
);
createMileStone(
MilestoneActivityTemplate.Type.PROJECT,
"Supervisor and reviewer final grading report submitted",
"Final grading report.",
milestonePhaseTemplate5,
null
milestonePhaseTemplate5
);
createMileStone(
MilestoneActivityTemplate.Type.STUDENT,
"Grading completed",
"Grading completed by examiner.",
milestonePhaseTemplate5,
null
milestonePhaseTemplate5
);
}
private void createMileStone(
MilestoneActivityTemplate.Type type,
String title,
String description,
MilestonePhaseTemplate milestonePhaseTemplate
) {
createMileStone(type, title, description, milestonePhaseTemplate, null, null);
}
private void createMileStone(
MilestoneActivityTemplate.Type type,
String title,
String description,
MilestonePhaseTemplate milestonePhaseTemplate,
Event event
) {
createMileStone(type, title, description, milestonePhaseTemplate, null, event);
}
private void createMileStone(
MilestoneActivityTemplate.Type type,
String title,
String description,
MilestonePhaseTemplate milestonePhaseTemplate,
String code
tozh4728 marked this conversation as resolved Outdated

For the future it might be a good idea to add an overload to reduce the overall diff. It is also confusing to read method calls with many null parameters.

For the future it might be a good idea to add an overload to reduce the overall diff. It is also confusing to read method calls with many `null` parameters.
) {
createMileStone(type, title, description, milestonePhaseTemplate, code, null);
}
private void createMileStone(
MilestoneActivityTemplate.Type type,
String title,
String description,
MilestonePhaseTemplate milestonePhaseTemplate,
String code,
Event event
) {
MilestoneActivityTemplate milestoneActivityTemplate = new MilestoneActivityTemplate(type, title, description);
milestoneActivityTemplate.addProjectType(bachelorClass);
@ -2150,6 +2186,7 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
milestoneActivityTemplate.addProjectType(magisterClass);
milestoneActivityTemplate.setMilestonePhaseTemplate(milestonePhaseTemplate);
milestoneActivityTemplate.setCode(code);
milestoneActivityTemplate.setActivatedBy(event);
milestoneActivityTemplateService.save(milestoneActivityTemplate, milestonePhaseTemplate);
}

@ -0,0 +1,102 @@
package se.su.dsv.scipro.testdata.populators;
import jakarta.inject.Inject;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.Year;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import org.springframework.stereotype.Service;
import se.su.dsv.scipro.file.FileUpload;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.project.ProjectService;
import se.su.dsv.scipro.reviewing.ReviewerAssignmentService;
import se.su.dsv.scipro.reviewing.ReviewerCapacityService;
import se.su.dsv.scipro.reviewing.RoughDraftApproval;
import se.su.dsv.scipro.reviewing.RoughDraftApprovalService;
import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.testdata.BaseData;
import se.su.dsv.scipro.testdata.Factory;
import se.su.dsv.scipro.testdata.TestDataPopulator;
@Service
public class SplitProjectPopulator implements TestDataPopulator {
private final ProjectService projectService;
private final ReviewerCapacityService reviewerCapacityService;
private final RoughDraftApprovalService roughDraftApprovalService;
private final ReviewerAssignmentService reviewerAssignmentService;
@Inject
public SplitProjectPopulator(
ProjectService projectService,
ReviewerCapacityService reviewerCapacityService,
RoughDraftApprovalService roughDraftApprovalService,
ReviewerAssignmentService reviewerAssignmentService
) {
this.projectService = projectService;
this.reviewerCapacityService = reviewerCapacityService;
this.roughDraftApprovalService = roughDraftApprovalService;
this.reviewerAssignmentService = reviewerAssignmentService;
}
@Override
public void populate(BaseData baseData, Factory factory) {
User supervisor = factory.createSupervisor("Emil");
User author1 = factory.createAuthor("Scott");
User author2 = factory.createAuthor("Scarlett");
User reviewer = factory.createReviewer("Elias");
reviewerCapacityService.assignTarget(reviewer, new ReviewerCapacityService.Target(Year.now(), 3, 3, ""));
Project project = Project.builder()
.title("Operating System Boot Time Security")
.projectType(baseData.bachelor())
.startDate(LocalDate.now())
.headSupervisor(supervisor)
.reviewers(Set.of(reviewer))
.projectParticipants(Set.of(author1, author2))
.build();
projectService.save(project);
roughDraftApprovalService.requestApproval(project, dummyFile(), "Request review.");
reviewerAssignmentService.assignReviewer(project, reviewer);
Optional<RoughDraftApproval> optional = roughDraftApprovalService.findBy(project);
optional.ifPresent(rda -> rda.approve("Approve! Good Work!", Optional.empty()));
}
private FileUpload dummyFile() {
return new FileUpload() {
@Override
public String getFileName() {
return "dummy.tmp";
}
@Override
public String getContentType() {
return "text/plain";
}
@Override
public User getUploader() {
return null;
}
@Override
public long getSize() {
return 0;
}
@Override
public <T> T handleData(Function<InputStream, T> handler) {
return handler.apply(InputStream.nullInputStream());
}
};
}
}

@ -294,6 +294,8 @@ public class SciProApplication extends LifecycleManagedWebApplication {
mountPage("admin/maintenance", SystemMaintenancePage.class);
mountPage("admin/project", ProjectManagementPage.class);
mountPage("admin/project/create", AdminCreateProjectPage.class);
mountPage("admin/project/split", AdminSplitProjectPage.class);
mountPage("admin/project/viewparentproject", AdminViewParentProjectPage.class);
mountPage("admin/project/survey", AdminSurveyPage.class);
mountPage("admin/project/reviewer", AdminAssignReviewerPage.class);
mountPage("admin/project/reviewer/capacity", AdminReviewerCapacityManagementPage.class);

@ -49,6 +49,9 @@
</form>
</div>
</div>
<wicket:container wicket:id="splitPanel"/>
</wicket:extend>
</body>
</html>

@ -3,14 +3,28 @@ package se.su.dsv.scipro.admin.pages;
import com.google.common.eventbus.EventBus;
import jakarta.inject.Inject;
import java.time.LocalDate;
import java.util.*;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.TreeSet;
import org.apache.wicket.RestartResponseException;
import org.apache.wicket.markup.html.form.*;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.markup.html.form.LambdaChoiceRenderer;
import org.apache.wicket.markup.html.form.RequiredTextField;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LambdaModel;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import se.su.dsv.scipro.components.*;
import se.su.dsv.scipro.components.AutoCompleteRoleProvider;
import se.su.dsv.scipro.components.BootstrapDatePicker;
import se.su.dsv.scipro.components.DatesValidator;
import se.su.dsv.scipro.components.DefaultSelect2MultiChoice;
import se.su.dsv.scipro.components.ResearchAreaChoiceRenderer;
import se.su.dsv.scipro.components.ResearchAreasModel;
import se.su.dsv.scipro.components.SupervisorAutoComplete;
import se.su.dsv.scipro.components.menuhighlighting.MenuHighlightAdminProjectManagement;
import se.su.dsv.scipro.data.DetachableServiceModel;
import se.su.dsv.scipro.data.DetachableServiceModelCollection;
@ -26,7 +40,13 @@ import se.su.dsv.scipro.project.ProjectService;
import se.su.dsv.scipro.project.ReviewerAssignedEvent;
import se.su.dsv.scipro.security.auth.Authorization;
import se.su.dsv.scipro.security.auth.roles.Roles;
import se.su.dsv.scipro.system.*;
import se.su.dsv.scipro.system.ProjectType;
import se.su.dsv.scipro.system.ProjectTypeService;
import se.su.dsv.scipro.system.ResearchArea;
import se.su.dsv.scipro.system.ResearchAreaService;
import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.system.UserSearchService;
import se.su.dsv.scipro.system.UserService;
import se.su.dsv.scipro.util.PageParameterKeys;
@Authorization(authorizedRoles = { Roles.SYSADMIN })
@ -74,7 +94,10 @@ public class AdminEditProjectPage extends AbstractAdminProjectPage implements Me
if (project == null) {
throw new RestartResponseException(AdminCreateProjectPage.class);
}
add(new ProjectForm(FORM, new DetachableServiceModel<>(projectService, project)));
DetachableServiceModel<Project> dsModel = new DetachableServiceModel<>(projectService, project);
add(new ProjectForm(FORM, dsModel));
add(new AdminSplitProjectPanel("splitPanel", dsModel));
}
private class ProjectForm extends Form<Project> {

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org" lang="en">
<body>
<wicket:extend>
<div class="row">
<div class="col-lg-5">
<h4 wicket:id="projectTitle"></h4>
<form class="form-horizontal" wicket:id="splitProjectForm">
<p>This project will be split between following authors:</p>
<div class="mb-3">
<ul wicket:id="authorList">
<li wicket:id="author"></li>
</ul>
</div>
<button class="btn btn-success" type="submit">Split Project</button>
&nbsp;&nbsp;
<a wicket:id="cancelLink">Cancel</a>
</form>
</div>
</div>
</wicket:extend>
</body>
</html>

@ -0,0 +1,73 @@
package se.su.dsv.scipro.admin.pages;
import jakarta.inject.Inject;
import java.util.ArrayList;
import org.apache.wicket.RestartResponseException;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.model.IModel;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import se.su.dsv.scipro.components.menuhighlighting.MenuHighlightAdminProjectManagement;
import se.su.dsv.scipro.data.DetachableServiceModel;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.project.ProjectService;
import se.su.dsv.scipro.project.split.SplitOrRestartProjectService;
import se.su.dsv.scipro.security.auth.Authorization;
import se.su.dsv.scipro.security.auth.roles.Roles;
import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.util.PageParameterKeys;
@Authorization(authorizedRoles = { Roles.SYSADMIN })
public class AdminSplitProjectPage extends AbstractAdminProjectPage implements MenuHighlightAdminProjectManagement {
@Inject
private ProjectService projectService;
@Inject
private SplitOrRestartProjectService splitOrRestartProjectService;
public AdminSplitProjectPage(PageParameters pp) {
final long id = pp.get(PageParameterKeys.MAP.get(Project.class)).toLong(0);
final Project project = projectService.findOne(id);
if (project == null) {
throw new RestartResponseException(AdminCreateProjectPage.class);
}
DetachableServiceModel<Project> dsModel = new DetachableServiceModel<>(projectService, project);
add(new Label("projectTitle", dsModel.map(Project::getTitle)));
add(new SplitProjectForm("splitProjectForm", dsModel));
}
private class SplitProjectForm extends Form<Project> {
public SplitProjectForm(String id, final IModel<Project> model) {
super(id, model);
add(
new ListView<>("authorList", model.map(Project::getProjectParticipants).map(ArrayList::new)) {
@Override
protected void populateItem(ListItem<User> item) {
item.add(new Label("author", item.getModel().map(User::getFullName)));
}
}
);
add(new BookmarkablePageLink<Void>("cancelLink", ProjectManagementPage.class));
}
@Override
protected void onSubmit() {
Long projectId = getModel().getObject().getId();
splitOrRestartProjectService.splitProject(projectId);
final PageParameters pp = new PageParameters();
pp.set(PageParameterKeys.MAP.get(Project.class), projectId);
setResponsePage(AdminViewParentProjectPage.class, pp);
}
}
}

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
<body>
<wicket:panel>
<div class="row mt-5">
<div class="col-md-12 col-lg-8 col-xl-5">
<p>
<a class="btn" wicket:id="splitProjectLink"><wicket:message key="splitButton" /></a>
</p>
<p wicket:id="splitInfo"></p>
</div>
</div>
</wicket:panel>
</body>
</html>

@ -0,0 +1,78 @@
package se.su.dsv.scipro.admin.pages;
import static se.su.dsv.scipro.project.split.SplitOrRestartProjectService.SplittableStatus;
import jakarta.inject.Inject;
import org.apache.wicket.behavior.AttributeAppender;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.AbstractLink;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.project.split.SplitOrRestartProjectService;
import se.su.dsv.scipro.security.auth.roles.Roles;
import se.su.dsv.scipro.session.SciProSession;
import se.su.dsv.scipro.util.PageParameterKeys;
public class AdminSplitProjectPanel extends Panel {
@Inject
private SplitOrRestartProjectService splitOrRestartProjectService;
public AdminSplitProjectPanel(String id, final IModel<Project> projectModel) {
super(id, projectModel);
Project project = projectModel.getObject();
LoadableDetachableModel<SplittableStatus> ldModel = LoadableDetachableModel.of(() -> {
SplitOrRestartProjectService.SplittableStatusRecord status =
splitOrRestartProjectService.getSplittableStatus(project.getId());
return status.splittableStatus();
});
final PageParameters pp = new PageParameters();
pp.set(PageParameterKeys.MAP.get(Project.class), project.getId());
AbstractLink splitProjectLink = new BookmarkablePageLink<Void>(
"splitProjectLink",
AdminSplitProjectPage.class,
pp
) {
@Override
protected void onConfigure() {
super.onConfigure();
if (ldModel.getObject() == SplittableStatus.OK) {
setEnabled(true);
this.add(new AttributeAppender("class", Model.of(" btn-success")));
} else {
setEnabled(false);
this.add(new AttributeAppender("class", Model.of(" btn-secondary disabled")));
}
}
};
add(splitProjectLink);
add(new Label("splitInfo", Model.of(getStatusMessage(ldModel.getObject()))));
Review

Replace with ldModel.map(status -> getStatusMessage(status)) to not get stale information when refreshing the page.

Otherwise I'm impressed with how well you've handled the IModel concept. Both when working with components model objects and also onConfigure and the like.

Replace with `ldModel.map(status -> getStatusMessage(status))` to not get stale information when refreshing the page. Otherwise I'm impressed with how well you've handled the `IModel` concept. Both when working with components model objects and also `onConfigure` and the like.
}
@Override
protected void onConfigure() {
super.onConfigure();
setVisibilityAllowed(SciProSession.get().authorizedForRole(Roles.ADMIN));
}
private String getStatusMessage(SplittableStatus status) {
return switch (status) {
case OK -> "";
case NOT_EXIST -> getString("status_not_exist");
case NOT_ACTIVE -> getString("status_not_active");
case NOT_TWO_PARTICIPANTS -> getString("status_not_two_participants");
case PHASE_TWO_STARTED -> getString("status_phase_two_started");
case FINAL_SEMINAR_PHASE_STARTED -> getString("status_final_seminar_phase_started");
};
}
}

@ -0,0 +1,6 @@
splitButton = Split Project
status_not_exit = Project does not exist.
status_not_active = Only active project can be split.
status_not_two_participants = To be able to split a project, it needs to have 2 participants.
status_phase_two_started = Phase 2 (Review) is already started, can't split right now.
status_final_seminar_phase_started = Final seminar phase has been started, too late to split.

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org" lang="en">
<body>
<wicket:extend>
<div class="row">
<div class="col-lg-5">
<h4 wicket:id="projectTitle"></h4>
<p>The project has following children projects:</p>
<div class="mb-3">
<ul wicket:id="projectList">
<li><a wicket:id="editLink"></a></li>
</ul>
</div>
<div class="mt-5">
<a class="btn btn-success" wicket:id="link">Projects</a>
</div>
</div>
</div>
</wicket:extend>
</body>
</html>

@ -0,0 +1,68 @@
package se.su.dsv.scipro.admin.pages;
import jakarta.inject.Inject;
import java.util.List;
import org.apache.wicket.RestartResponseException;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import se.su.dsv.scipro.components.menuhighlighting.MenuHighlightAdminProjectManagement;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.project.ProjectService;
import se.su.dsv.scipro.project.split.SplitOrRestartProjectService;
import se.su.dsv.scipro.security.auth.Authorization;
import se.su.dsv.scipro.security.auth.roles.Roles;
import se.su.dsv.scipro.util.PageParameterKeys;
@Authorization(authorizedRoles = { Roles.SYSADMIN })
public class AdminViewParentProjectPage
extends AbstractAdminProjectPage
implements MenuHighlightAdminProjectManagement {
@Inject
private ProjectService projectService;
@Inject
private SplitOrRestartProjectService splitOrRestartProjectService;
public AdminViewParentProjectPage(PageParameters pp) {
final long id = pp.get(PageParameterKeys.MAP.get(Project.class)).toLong(0);
final Project project = projectService.findOne(id);
if (project == null) {
throw new RestartResponseException(AdminCreateProjectPage.class);
}
add(new Label("projectTitle", Model.of(project.getTitle())));
LoadableDetachableModel<List<Project>> ldModel = LoadableDetachableModel.of(() ->
splitOrRestartProjectService.getChildProjects(id)
);
add(
new ListView<>("projectList", ldModel) {
@Override
protected void populateItem(ListItem<Project> item) {
Project project = item.getModelObject();
final PageParameters pp = new PageParameters();
pp.set(PageParameterKeys.MAP.get(Project.class), project.getId());
BookmarkablePageLink<Void> link = new BookmarkablePageLink<Void>(
"editLink",
AdminEditProjectPage.class,
pp
);
link.setBody(Model.of(project.getTitle() + " - " + project.getAuthorNames()));
Review

Should use item.getModel().map(project -> ...) again to prevent possibility of stale data.

Should use `item.getModel().map(project -> ...)` again to prevent possibility of stale data.
item.add(link);
}
}
);
add(new BookmarkablePageLink<Void>("link", ProjectManagementPage.class));
}
}

@ -97,6 +97,7 @@ import se.su.dsv.scipro.project.ProjectNoteService;
import se.su.dsv.scipro.project.ProjectPeopleStatisticsService;
import se.su.dsv.scipro.project.ProjectService;
import se.su.dsv.scipro.project.pages.ProjectStartPage;
import se.su.dsv.scipro.project.split.SplitOrRestartProjectService;
import se.su.dsv.scipro.projectpartner.ProjectPartnerService;
import se.su.dsv.scipro.reflection.ReflectionService;
import se.su.dsv.scipro.report.GradeCalculatorService;
@ -252,6 +253,9 @@ public abstract class SciProTest {
@Mock
protected ProjectService projectService;
@Mock
protected SplitOrRestartProjectService splitOrRestartProjectService;
@Mock
protected ResearchAreaService researchAreaService;

@ -20,11 +20,13 @@ import se.su.dsv.scipro.mail.MailEvent;
import se.su.dsv.scipro.notifications.dataobject.NotificationSource;
import se.su.dsv.scipro.notifications.dataobject.ProjectEvent;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.project.split.SplitOrRestartProjectService;
import se.su.dsv.scipro.security.auth.roles.Roles;
import se.su.dsv.scipro.system.DegreeType;
import se.su.dsv.scipro.system.ProjectType;
import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.util.PageParameterKeys;
import se.su.dsv.scipro.util.Pair;
public class AdminEditProjectPageTest extends SciProTest {
@ -269,6 +271,16 @@ public class AdminEditProjectPageTest extends SciProTest {
private void startPage(Project project) {
if (project.getId() != null) when(projectService.findOne(project.getId())).thenReturn(project);
lenient()
.when(splitOrRestartProjectService.getSplittableStatus(project.getId() != null ? project.getId() : 0L))
.thenReturn(
new SplitOrRestartProjectService.SplittableStatusRecord(
SplitOrRestartProjectService.SplittableStatus.OK,
project,
null
)
);
PageParameters pp = new PageParameters();
pp.set(PageParameterKeys.MAP.get(Project.class), project.getId());
tester.startPage(AdminEditProjectPage.class, pp);