Split Project #146

Open
tozh4728 wants to merge 46 commits from 87-split-project into develop
29 changed files with 1162 additions and 51 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.ProjectRepo;
import se.su.dsv.scipro.project.ProjectService; import se.su.dsv.scipro.project.ProjectService;
import se.su.dsv.scipro.project.ProjectServiceImpl; 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.projectpartner.ProjectPartnerServiceImpl;
import se.su.dsv.scipro.reflection.ReflectionService; import se.su.dsv.scipro.reflection.ReflectionService;
import se.su.dsv.scipro.reflection.ReflectionServiceImpl; import se.su.dsv.scipro.reflection.ReflectionServiceImpl;
@ -814,6 +815,23 @@ public class CoreConfig {
return new ProjectServiceImpl(projectRepo, clock, eventBus, em); return new ProjectServiceImpl(projectRepo, clock, eventBus, em);
} }
@Bean
public SplitOrRestartProjectServiceImpl SplitOrRestartProjectService(
ProjectService projectService,
FinalSeminarService finalSeminarService,
RoughDraftApprovalService roughDraftApprovalService,
Clock clock,
EventBus eventBus
) {
return new SplitOrRestartProjectServiceImpl(
projectService,
finalSeminarService,
roughDraftApprovalService,
clock,
eventBus
);
}
@Bean @Bean
public ProjectTypeServiceImpl projectTypeService(Provider<EntityManager> em) { public ProjectTypeServiceImpl projectTypeService(Provider<EntityManager> em) {
return new ProjectTypeServiceImpl(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.project.Project;
import se.su.dsv.scipro.report.SupervisorGradingReportSubmittedEvent; import se.su.dsv.scipro.report.SupervisorGradingReportSubmittedEvent;
import se.su.dsv.scipro.reviewing.FinalSeminarApprovalApprovedEvent; 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.RoughDraftApprovalApprovedEvent;
import se.su.dsv.scipro.reviewing.RoughDraftApprovalRequestedEvent; import se.su.dsv.scipro.reviewing.RoughDraftApprovalRequestedEvent;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
@ -136,6 +137,11 @@ public class MilestoneActivator {
activateProjectMilestone(Set.of(event.getName()), event.getProject()); activateProjectMilestone(Set.of(event.getName()), event.getProject());
} }
@Subscribe
public void reviewerApprovalApprovedClone(RoughDraftApprovalApprovedClonedEvent event) {
activateProjectMilestone(Set.of(event.getName()), event.getProject());
}
@Subscribe @Subscribe
public void finalSeminarThesisDeleted(FinalSeminarThesisDeletedEvent event) { public void finalSeminarThesisDeleted(FinalSeminarThesisDeletedEvent event) {
deactivateProjectMilestone(Set.of("FinalSeminarThesisUploaded"), event.getFinalSeminar().getProject()); deactivateProjectMilestone(Set.of("FinalSeminarThesisUploaded"), event.getFinalSeminar().getProject());

@ -23,6 +23,7 @@ import jakarta.persistence.MapKeyJoinColumn;
import jakarta.persistence.PrePersist; import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate; import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@ -113,6 +114,18 @@ public class Project extends DomainObject {
@Column(name = "daisy_identifier", unique = true) @Column(name = "daisy_identifier", unique = true)
private Integer identifier; 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 // Embedded JPA-mapping
// ---------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------
@ -365,6 +378,30 @@ public class Project extends DomainObject {
this.userNotes = userNotes; 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 // 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,133 @@
package se.su.dsv.scipro.project.split;
import com.google.common.eventbus.EventBus;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.time.Clock;
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 Clock clock;
private final EventBus eventBus;
@Inject
public SplitOrRestartProjectServiceImpl(
ProjectService projectService,
FinalSeminarService finalSeminarService,
RoughDraftApprovalService roughDraftApprovalService,
Clock clock,
EventBus eventBus
) {
this.projectService = projectService;
this.finalSeminarService = finalSeminarService;
this.roughDraftApprovalService = roughDraftApprovalService;
this.clock = clock;
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, clock.instant())
);
// 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()); 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( private void decideWithDecisionDate(
final Status status, final Status status,
final String reason, final String reason,

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

@ -1,6 +1,8 @@
package se.su.dsv.scipro.reviewing; package se.su.dsv.scipro.reviewing;
import jakarta.persistence.Basic;
import jakarta.persistence.CascadeType; import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.DiscriminatorColumn;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
@ -11,6 +13,7 @@ import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne; import jakarta.persistence.OneToOne;
import jakarta.persistence.OrderBy; import jakarta.persistence.OrderBy;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.Instant;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.LinkedList; import java.util.LinkedList;
@ -32,6 +35,14 @@ public abstract class ReviewerApproval extends DomainObject {
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; 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 // JPA-mappings of foreign keys in this table (reviewer_approval) referencing other
// tables. // tables.
@ -59,6 +70,22 @@ public abstract class ReviewerApproval extends DomainObject {
return this.project; 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 // Other methods
// ---------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------

@ -1,7 +1,8 @@
package se.su.dsv.scipro.reviewing; package se.su.dsv.scipro.reviewing;
import jakarta.persistence.Entity; 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.file.FileReference;
import se.su.dsv.scipro.project.Project; import se.su.dsv.scipro.project.Project;
@ -24,4 +25,15 @@ public class RoughDraftApproval extends ReviewerApproval {
public Step getStep() { public Step getStep() {
return Step.ROUGH_DRAFT_APPROVAL; return Step.ROUGH_DRAFT_APPROVAL;
} }
public RoughDraftApproval cloneToProject(final Project newProject, Instant instant) {
RoughDraftApproval rda = new RoughDraftApproval();
rda.project = newProject;
this.decisions.forEach(decision -> rda.decisions.add(decision.cloneToReviewerApproval(rda)));
rda.isCloned = true;
rda.cloneTimestamp = instant;
tozh4728 marked this conversation as resolved Outdated

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; 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) { public Optional<RoughDraftApproval> findBy(Project project) {
return Optional.ofNullable(findOne(QRoughDraftApproval.roughDraftApproval.project.eq(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,296 @@
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.ZoneId;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
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.FinalSeminarApprovalService;
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.security.auth.roles.Roles;
import se.su.dsv.scipro.system.DegreeType;
import se.su.dsv.scipro.system.Language;
import se.su.dsv.scipro.system.ProjectType;
import se.su.dsv.scipro.system.ResearchArea;
import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.test.IntegrationTest;
import se.su.dsv.scipro.test.MutableFixedClock;
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
MutableFixedClock clock;
@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;
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).
@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();
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() {
tozh4728 marked this conversation as resolved Outdated

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.
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()
);
assertEquals(REMAINING_TARGET, remainingTargets.spring());
assertEquals(TARGET, remainingTargets.autumn());
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());
});
}
tozh4728 marked this conversation as resolved Outdated

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.
@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()
);
assertEquals(REMAINING_TARGET, remainingTargets.spring());
assertEquals(TARGET, remainingTargets.autumn());
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);
LocalDate plus30Days = LocalDate.now(clock).plusDays(30);
finalSeminar.setStartDate(Date.from(plus30Days.atStartOfDay(ZoneId.systemDefault()).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().withMonth(1).withDayOfMonth(1));
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);
clock.setDate(LocalDate.now().withMonth(3).withDayOfMonth(1));
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());
}
};
}
}

@ -6,10 +6,7 @@ import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStream; import java.io.InputStream;
import java.time.LocalDate; import java.time.*;
import java.time.LocalTime;
import java.time.Month;
import java.time.ZonedDateTime;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Function;
import se.su.dsv.scipro.checklist.ChecklistCategory; import se.su.dsv.scipro.checklist.ChecklistCategory;
@ -28,6 +25,7 @@ import se.su.dsv.scipro.match.TholanderBox;
import se.su.dsv.scipro.milestones.dataobjects.MilestoneActivityTemplate; import se.su.dsv.scipro.milestones.dataobjects.MilestoneActivityTemplate;
import se.su.dsv.scipro.milestones.dataobjects.MilestonePhaseTemplate; import se.su.dsv.scipro.milestones.dataobjects.MilestonePhaseTemplate;
import se.su.dsv.scipro.milestones.service.MilestoneActivityTemplateService; import se.su.dsv.scipro.milestones.service.MilestoneActivityTemplateService;
import se.su.dsv.scipro.milestones.service.MilestonePhaseTemplateService;
import se.su.dsv.scipro.notifications.dataobject.CustomEvent; import se.su.dsv.scipro.notifications.dataobject.CustomEvent;
import se.su.dsv.scipro.notifications.dataobject.GroupEvent; import se.su.dsv.scipro.notifications.dataobject.GroupEvent;
import se.su.dsv.scipro.notifications.dataobject.IdeaEvent; import se.su.dsv.scipro.notifications.dataobject.IdeaEvent;
@ -73,6 +71,12 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
@Inject @Inject
private MilestoneActivityTemplateService milestoneActivityTemplateService; private MilestoneActivityTemplateService milestoneActivityTemplateService;
@Inject
private MilestonePhaseTemplateService milestonePhaseTemplateService;
@Inject
private EventService eventService;
@Inject @Inject
private FileService fileService; private FileService fileService;
@ -2016,34 +2020,37 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
MilestoneActivityTemplate.Type.STUDENT, MilestoneActivityTemplate.Type.STUDENT,
"First meeting held", "First meeting held",
"First meeting with supervisor.", "First meeting with supervisor.",
milestonePhaseTemplate1, milestonePhaseTemplate1
null
);
createMileStone(
MilestoneActivityTemplate.Type.PROJECT,
"Project plan approved",
"Project plan approved by supervisor.",
milestonePhaseTemplate1,
null
); );
createMileStone( createMileStone(
MilestoneActivityTemplate.Type.PROJECT, MilestoneActivityTemplate.Type.PROJECT,
"Rough draft sent to reviewer for approval", "Project plan approved",
"Rough draft approved by reviewer.", "Project plan approved by supervisor.",
milestonePhaseTemplate2, milestonePhaseTemplate1
null
); );
List<Event> events = eventService.findAll();
createMileStone( createMileStone(
MilestoneActivityTemplate.Type.PROJECT, 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.", "Rough draft approved.",
milestonePhaseTemplate2, milestonePhaseTemplate2,
null events.stream().filter(event -> event.getName().equals("Step.ROUGH_DRAFT_APPROVAL")).findFirst().get()
); );
createMileStone( createMileStone(
MilestoneActivityTemplate.Type.STUDENT, MilestoneActivityTemplate.Type.STUDENT,
"Peer review 1", "Peer review 1 (Auto)",
"This is a recommendation of when to perform peer review 1.", "This is a recommendation of when to perform peer review 1.",
milestonePhaseTemplate2, milestonePhaseTemplate2,
MilestoneActivityTemplate.PEER_REVIEW_ONE MilestoneActivityTemplate.PEER_REVIEW_ONE
@ -2053,9 +2060,9 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
MilestoneActivityTemplate.Type.PROJECT, MilestoneActivityTemplate.Type.PROJECT,
"Result and discussion completed and approved", "Result and discussion completed and approved",
"Result and discussion.", "Result and discussion.",
milestonePhaseTemplate3, milestonePhaseTemplate3
null
); );
createMileStone( createMileStone(
MilestoneActivityTemplate.Type.STUDENT, MilestoneActivityTemplate.Type.STUDENT,
"Peer review 2", "Peer review 2",
@ -2068,9 +2075,9 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
MilestoneActivityTemplate.Type.PROJECT, MilestoneActivityTemplate.Type.PROJECT,
"Thesis approved for final seminar presentation", "Thesis approved for final seminar presentation",
"Thesis approved for final seminar.", "Thesis approved for final seminar.",
milestonePhaseTemplate4, milestonePhaseTemplate4
null
); );
createMileStone( createMileStone(
MilestoneActivityTemplate.Type.PROJECT, MilestoneActivityTemplate.Type.PROJECT,
"Final seminar created", "Final seminar created",
@ -2078,6 +2085,7 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
milestonePhaseTemplate4, milestonePhaseTemplate4,
MilestoneActivityTemplate.CREATE_SEMINAR MilestoneActivityTemplate.CREATE_SEMINAR
); );
createMileStone( createMileStone(
MilestoneActivityTemplate.Type.PROJECT, MilestoneActivityTemplate.Type.PROJECT,
"Final seminar thesis uploaded", "Final seminar thesis uploaded",
@ -2085,77 +2093,111 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
milestonePhaseTemplate4, milestonePhaseTemplate4,
MilestoneActivityTemplate.THESIS_UPLOADED MilestoneActivityTemplate.THESIS_UPLOADED
); );
createMileStone( createMileStone(
MilestoneActivityTemplate.Type.STUDENT, MilestoneActivityTemplate.Type.STUDENT,
"Perform an oral and written opposition", "Perform an oral and written opposition",
"Opposition.", "Opposition.",
milestonePhaseTemplate4, milestonePhaseTemplate4
null
); );
createMileStone( createMileStone(
MilestoneActivityTemplate.Type.STUDENT, MilestoneActivityTemplate.Type.STUDENT,
"Active participation in a final seminar", "Active participation in a final seminar",
"Active participation.", "Active participation.",
milestonePhaseTemplate4, milestonePhaseTemplate4
null
); );
createMileStone( createMileStone(
MilestoneActivityTemplate.Type.STUDENT, MilestoneActivityTemplate.Type.STUDENT,
"Defend the thesis in a final seminar", "Defend the thesis in a final seminar",
"Defence of final thesis.", "Defence of final thesis.",
milestonePhaseTemplate4, milestonePhaseTemplate4
null
); );
createMileStone( createMileStone(
MilestoneActivityTemplate.Type.PROJECT, MilestoneActivityTemplate.Type.PROJECT,
"Revised final thesis of the submitted thesis", "Revised final thesis of the submitted thesis",
"Revised final thesis.", "Revised final thesis.",
milestonePhaseTemplate5, milestonePhaseTemplate5
null
); );
createMileStone( createMileStone(
MilestoneActivityTemplate.Type.PROJECT, MilestoneActivityTemplate.Type.PROJECT,
"Originality report approved", "Originality report approved",
"Originality report.", "Originality report.",
milestonePhaseTemplate5, milestonePhaseTemplate5
null
); );
createMileStone( createMileStone(
MilestoneActivityTemplate.Type.PROJECT, MilestoneActivityTemplate.Type.PROJECT,
"Supervisor and reviewer final grading report submitted", "Supervisor and reviewer final grading report submitted",
"Final grading report.", "Final grading report.",
milestonePhaseTemplate5, milestonePhaseTemplate5
null
); );
createMileStone( createMileStone(
MilestoneActivityTemplate.Type.STUDENT, MilestoneActivityTemplate.Type.STUDENT,
"Grading completed", "Grading completed",
"Grading completed by examiner.", "Grading completed by examiner.",
milestonePhaseTemplate5, milestonePhaseTemplate5
null
); );
} }
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( private void createMileStone(
MilestoneActivityTemplate.Type type, MilestoneActivityTemplate.Type type,
String title, String title,
String description, String description,
MilestonePhaseTemplate milestonePhaseTemplate, MilestonePhaseTemplate milestonePhaseTemplate,
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.
String code String code
) {
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 milestoneActivityTemplate = new MilestoneActivityTemplate(type, title, description);
LocalDate ld = LocalDate.now().minusYears(1);
milestoneActivityTemplate.setDateCreated(Date.from(ld.atStartOfDay(ZoneId.systemDefault()).toInstant()));
milestoneActivityTemplate.addProjectType(bachelorClass); milestoneActivityTemplate.addProjectType(bachelorClass);
milestoneActivityTemplate.addProjectType(masterClass); milestoneActivityTemplate.addProjectType(masterClass);
milestoneActivityTemplate.addProjectType(magisterClass); milestoneActivityTemplate.addProjectType(magisterClass);
milestoneActivityTemplate.setMilestonePhaseTemplate(milestonePhaseTemplate); milestoneActivityTemplate.setMilestonePhaseTemplate(milestonePhaseTemplate);
milestoneActivityTemplate.setCode(code); milestoneActivityTemplate.setCode(code);
milestoneActivityTemplate.setActivatedBy(event);
milestoneActivityTemplateService.save(milestoneActivityTemplate, milestonePhaseTemplate); milestoneActivityTemplateService.save(milestoneActivityTemplate, milestonePhaseTemplate);
} }
private MilestonePhaseTemplate createMileStonePhase(String title, String description) { private MilestonePhaseTemplate createMileStonePhase(String title, String description) {
MilestonePhaseTemplate milestonePhaseTemplate1 = new MilestonePhaseTemplate(title, description); MilestonePhaseTemplate milestonePhaseTemplate = new MilestonePhaseTemplate(title, description);
return save(milestonePhaseTemplate1); LocalDate ld = LocalDate.now().minusYears(1);
milestonePhaseTemplate.setDateCreated(Date.from(ld.atStartOfDay(ZoneId.systemDefault()).toInstant()));
return milestonePhaseTemplateService.save(milestonePhaseTemplate);
} }
private <T> T save(T entity) { private <T> T save(T entity) {

@ -0,0 +1,106 @@
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.ReviewerDecisionService;
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;
private final ReviewerDecisionService reviewerDecisionService;
@Inject
public SplitProjectPopulator(
ProjectService projectService,
ReviewerCapacityService reviewerCapacityService,
RoughDraftApprovalService roughDraftApprovalService,
ReviewerAssignmentService reviewerAssignmentService,
ReviewerDecisionService reviewerDecisionService
) {
this.projectService = projectService;
this.reviewerCapacityService = reviewerCapacityService;
this.roughDraftApprovalService = roughDraftApprovalService;
this.reviewerAssignmentService = reviewerAssignmentService;
this.reviewerDecisionService = reviewerDecisionService;
}
@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 -> reviewerDecisionService.approve(rda, "Approved! 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/maintenance", SystemMaintenancePage.class);
mountPage("admin/project", ProjectManagementPage.class); mountPage("admin/project", ProjectManagementPage.class);
mountPage("admin/project/create", AdminCreateProjectPage.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/survey", AdminSurveyPage.class);
mountPage("admin/project/reviewer", AdminAssignReviewerPage.class); mountPage("admin/project/reviewer", AdminAssignReviewerPage.class);
mountPage("admin/project/reviewer/capacity", AdminReviewerCapacityManagementPage.class); mountPage("admin/project/reviewer/capacity", AdminReviewerCapacityManagementPage.class);

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

@ -3,14 +3,28 @@ package se.su.dsv.scipro.admin.pages;
import com.google.common.eventbus.EventBus; import com.google.common.eventbus.EventBus;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import java.time.LocalDate; 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.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.markup.html.panel.FeedbackPanel;
import org.apache.wicket.model.IModel; import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LambdaModel; import org.apache.wicket.model.LambdaModel;
import org.apache.wicket.request.mapper.parameter.PageParameters; 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.components.menuhighlighting.MenuHighlightAdminProjectManagement;
import se.su.dsv.scipro.data.DetachableServiceModel; import se.su.dsv.scipro.data.DetachableServiceModel;
import se.su.dsv.scipro.data.DetachableServiceModelCollection; 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.project.ReviewerAssignedEvent;
import se.su.dsv.scipro.security.auth.Authorization; import se.su.dsv.scipro.security.auth.Authorization;
import se.su.dsv.scipro.security.auth.roles.Roles; 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; import se.su.dsv.scipro.util.PageParameterKeys;
@Authorization(authorizedRoles = { Roles.SYSADMIN }) @Authorization(authorizedRoles = { Roles.SYSADMIN })
@ -74,7 +94,10 @@ public class AdminEditProjectPage extends AbstractAdminProjectPage implements Me
if (project == null) { if (project == null) {
throw new RestartResponseException(AdminCreateProjectPage.class); 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> { 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", ldModel.map(this::getStatusMessage)));
tozh4728 marked this conversation as resolved Outdated

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,67 @@
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(item.getModel().map(p -> p.getTitle() + " - " + p.getAuthorNames()));
tozh4728 marked this conversation as resolved Outdated

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));
}
}

@ -83,10 +83,11 @@ public abstract class ApprovalReviewerPanel extends Panel {
LinkWrapper.apply(componentId, id -> { LinkWrapper.apply(componentId, id -> {
AbstractLink link = newDecisionLink(id, rowModel.map(Decision::getReviewerApproval)); AbstractLink link = newDecisionLink(id, rowModel.map(Decision::getReviewerApproval));
link.setBody( link.setBody(
rowModel rowModel.map(dec -> {
.map(Decision::getReviewerApproval) ReviewerApproval ra = dec.getReviewerApproval();
.map(ReviewerApproval::getProject) Project p = ra.getProject();
.map(Project::getTitle) return ra.getCloned() ? p.getTitle() + " (Cloned)" : p.getTitle();
})
); );
return link; return link;
}) })

@ -97,6 +97,7 @@ import se.su.dsv.scipro.project.ProjectNoteService;
import se.su.dsv.scipro.project.ProjectPeopleStatisticsService; import se.su.dsv.scipro.project.ProjectPeopleStatisticsService;
import se.su.dsv.scipro.project.ProjectService; import se.su.dsv.scipro.project.ProjectService;
import se.su.dsv.scipro.project.pages.ProjectStartPage; 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.projectpartner.ProjectPartnerService;
import se.su.dsv.scipro.reflection.ReflectionService; import se.su.dsv.scipro.reflection.ReflectionService;
import se.su.dsv.scipro.report.GradeCalculatorService; import se.su.dsv.scipro.report.GradeCalculatorService;
@ -252,6 +253,9 @@ public abstract class SciProTest {
@Mock @Mock
protected ProjectService projectService; protected ProjectService projectService;
@Mock
protected SplitOrRestartProjectService splitOrRestartProjectService;
@Mock @Mock
protected ResearchAreaService researchAreaService; 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.NotificationSource;
import se.su.dsv.scipro.notifications.dataobject.ProjectEvent; import se.su.dsv.scipro.notifications.dataobject.ProjectEvent;
import se.su.dsv.scipro.project.Project; 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.security.auth.roles.Roles;
import se.su.dsv.scipro.system.DegreeType; import se.su.dsv.scipro.system.DegreeType;
import se.su.dsv.scipro.system.ProjectType; import se.su.dsv.scipro.system.ProjectType;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.util.PageParameterKeys; import se.su.dsv.scipro.util.PageParameterKeys;
import se.su.dsv.scipro.util.Pair;
public class AdminEditProjectPageTest extends SciProTest { public class AdminEditProjectPageTest extends SciProTest {
@ -269,6 +271,16 @@ public class AdminEditProjectPageTest extends SciProTest {
private void startPage(Project project) { private void startPage(Project project) {
if (project.getId() != null) when(projectService.findOne(project.getId())).thenReturn(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(); PageParameters pp = new PageParameters();
pp.set(PageParameterKeys.MAP.get(Project.class), project.getId()); pp.set(PageParameterKeys.MAP.get(Project.class), project.getId());
tester.startPage(AdminEditProjectPage.class, pp); tester.startPage(AdminEditProjectPage.class, pp);