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.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,23 @@ public class CoreConfig {
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
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;
// ----------------------------------------------------------------------------------
// 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,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());
}
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;
// ----------------------------------------------------------------------------------
// 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, 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;
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,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() {
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());
});
}
@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 java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Month;
import java.time.ZonedDateTime;
import java.time.*;
import java.util.*;
import java.util.function.Function;
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.MilestonePhaseTemplate;
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.GroupEvent;
import se.su.dsv.scipro.notifications.dataobject.IdeaEvent;
@ -73,6 +71,12 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
@Inject
private MilestoneActivityTemplateService milestoneActivityTemplateService;
@Inject
private MilestonePhaseTemplateService milestonePhaseTemplateService;
@Inject
private EventService eventService;
@Inject
private FileService fileService;
@ -2016,34 +2020,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 +2060,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 +2075,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 +2085,7 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
milestonePhaseTemplate4,
MilestoneActivityTemplate.CREATE_SEMINAR
);
createMileStone(
MilestoneActivityTemplate.Type.PROJECT,
"Final seminar thesis uploaded",
@ -2085,77 +2093,111 @@ 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
) {
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);
LocalDate ld = LocalDate.now().minusYears(1);
milestoneActivityTemplate.setDateCreated(Date.from(ld.atStartOfDay(ZoneId.systemDefault()).toInstant()));
milestoneActivityTemplate.addProjectType(bachelorClass);
milestoneActivityTemplate.addProjectType(masterClass);
milestoneActivityTemplate.addProjectType(magisterClass);
milestoneActivityTemplate.setMilestonePhaseTemplate(milestonePhaseTemplate);
milestoneActivityTemplate.setCode(code);
milestoneActivityTemplate.setActivatedBy(event);
milestoneActivityTemplateService.save(milestoneActivityTemplate, milestonePhaseTemplate);
}
private MilestonePhaseTemplate createMileStonePhase(String title, String description) {
MilestonePhaseTemplate milestonePhaseTemplate1 = new MilestonePhaseTemplate(title, description);
return save(milestonePhaseTemplate1);
MilestonePhaseTemplate milestonePhaseTemplate = new MilestonePhaseTemplate(title, description);
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) {

@ -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/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", ldModel.map(this::getStatusMessage)));
}
@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()));
item.add(link);
}
}
);
add(new BookmarkablePageLink<Void>("link", ProjectManagementPage.class));
}
}

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