Split Project #146
@ -145,6 +145,7 @@ import se.su.dsv.scipro.project.ProjectPeopleStatisticsServiceImpl;
|
||||
import se.su.dsv.scipro.project.ProjectRepo;
|
||||
import se.su.dsv.scipro.project.ProjectService;
|
||||
import se.su.dsv.scipro.project.ProjectServiceImpl;
|
||||
import se.su.dsv.scipro.project.split.SplitOrRestartProjectServiceImpl;
|
||||
import se.su.dsv.scipro.projectpartner.ProjectPartnerServiceImpl;
|
||||
import se.su.dsv.scipro.reflection.ReflectionService;
|
||||
import se.su.dsv.scipro.reflection.ReflectionServiceImpl;
|
||||
@ -814,6 +815,21 @@ public class CoreConfig {
|
||||
return new ProjectServiceImpl(projectRepo, clock, eventBus, em);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SplitOrRestartProjectServiceImpl SplitOrRestartProjectService(
|
||||
ProjectService projectService,
|
||||
FinalSeminarService finalSeminarService,
|
||||
RoughDraftApprovalService roughDraftApprovalService,
|
||||
EventBus eventBus
|
||||
) {
|
||||
return new SplitOrRestartProjectServiceImpl(
|
||||
projectService,
|
||||
finalSeminarService,
|
||||
roughDraftApprovalService,
|
||||
eventBus
|
||||
);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ProjectTypeServiceImpl projectTypeService(Provider<EntityManager> em) {
|
||||
return new ProjectTypeServiceImpl(em);
|
||||
|
@ -21,6 +21,7 @@ import se.su.dsv.scipro.peer.SecondPeerReviewCompletedEvent;
|
||||
import se.su.dsv.scipro.project.Project;
|
||||
import se.su.dsv.scipro.report.SupervisorGradingReportSubmittedEvent;
|
||||
import se.su.dsv.scipro.reviewing.FinalSeminarApprovalApprovedEvent;
|
||||
import se.su.dsv.scipro.reviewing.RoughDraftApprovalApprovedClonedEvent;
|
||||
import se.su.dsv.scipro.reviewing.RoughDraftApprovalApprovedEvent;
|
||||
import se.su.dsv.scipro.reviewing.RoughDraftApprovalRequestedEvent;
|
||||
import se.su.dsv.scipro.system.User;
|
||||
@ -136,6 +137,11 @@ public class MilestoneActivator {
|
||||
activateProjectMilestone(Set.of(event.getName()), event.getProject());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void reviewerApprovalApprovedClone(RoughDraftApprovalApprovedClonedEvent event) {
|
||||
activateProjectMilestone(Set.of(event.getName()), event.getProject());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void finalSeminarThesisDeleted(FinalSeminarThesisDeletedEvent event) {
|
||||
deactivateProjectMilestone(Set.of("FinalSeminarThesisUploaded"), event.getFinalSeminar().getProject());
|
||||
|
@ -23,6 +23,7 @@ import jakarta.persistence.MapKeyJoinColumn;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.PreUpdate;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
@ -113,6 +114,18 @@ public class Project extends DomainObject {
|
||||
@Column(name = "daisy_identifier", unique = true)
|
||||
private Integer identifier;
|
||||
|
||||
@Basic
|
||||
@Column(name = "parent_project_id")
|
||||
private Long parentProjectId;
|
||||
|
||||
@Basic
|
||||
@Column(name = "root_project_id")
|
||||
private Long rootProjectId;
|
||||
|
||||
@Basic
|
||||
@Column(name = "clone_timestamp")
|
||||
private Instant cloneTimestamp;
|
||||
|
||||
// ----------------------------------------------------------------------------------
|
||||
tozh4728 marked this conversation as resolved
Outdated
|
||||
// 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);
|
||||
}
|
127
core/src/main/java/se/su/dsv/scipro/project/split/SplitOrRestartProjectServiceImpl.java
Normal file
@ -0,0 +1,127 @@
|
||||
package se.su.dsv.scipro.project.split;
|
||||
|
||||
import com.google.common.eventbus.EventBus;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import se.su.dsv.scipro.finalseminar.FinalSeminarService;
|
||||
import se.su.dsv.scipro.project.Project;
|
||||
import se.su.dsv.scipro.project.ProjectService;
|
||||
import se.su.dsv.scipro.project.ProjectStatus;
|
||||
import se.su.dsv.scipro.project.QProject;
|
||||
import se.su.dsv.scipro.reviewing.RoughDraftApproval;
|
||||
import se.su.dsv.scipro.reviewing.RoughDraftApprovalApprovedClonedEvent;
|
||||
import se.su.dsv.scipro.reviewing.RoughDraftApprovalService;
|
||||
import se.su.dsv.scipro.system.User;
|
||||
|
||||
public class SplitOrRestartProjectServiceImpl implements SplitOrRestartProjectService {
|
||||
|
||||
private final ProjectService projectService;
|
||||
private final FinalSeminarService finalSeminarService;
|
||||
private final RoughDraftApprovalService roughDraftApprovalService;
|
||||
private final EventBus eventBus;
|
||||
|
||||
@Inject
|
||||
public SplitOrRestartProjectServiceImpl(
|
||||
ProjectService projectService,
|
||||
FinalSeminarService finalSeminarService,
|
||||
RoughDraftApprovalService roughDraftApprovalService,
|
||||
EventBus eventBus
|
||||
) {
|
||||
this.projectService = projectService;
|
||||
this.finalSeminarService = finalSeminarService;
|
||||
this.roughDraftApprovalService = roughDraftApprovalService;
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public SplittableStatusRecord getSplittableStatus(long projectId) {
|
||||
Project project = projectService.findOne(projectId);
|
||||
if (project == null) return new SplittableStatusRecord(SplittableStatus.NOT_EXIST, null, null);
|
||||
|
||||
if (project.getProjectStatus() != ProjectStatus.ACTIVE) {
|
||||
return new SplittableStatusRecord(SplittableStatus.NOT_ACTIVE, project, null);
|
||||
}
|
||||
|
||||
if (project.getProjectParticipants().size() != 2) {
|
||||
return new SplittableStatusRecord(SplittableStatus.NOT_TWO_PARTICIPANTS, project, null);
|
||||
}
|
||||
|
||||
if (finalSeminarService.findByProject(project) != null) {
|
||||
return new SplittableStatusRecord(SplittableStatus.FINAL_SEMINAR_PHASE_STARTED, project, null);
|
||||
}
|
||||
|
||||
Optional<RoughDraftApproval> o = roughDraftApprovalService.findBy(project);
|
||||
if (o.isPresent() && !o.get().isDecided()) {
|
||||
return new SplittableStatusRecord(SplittableStatus.PHASE_TWO_STARTED, project, null);
|
||||
}
|
||||
|
||||
if (o.isPresent() && o.get().isApproved()) {
|
||||
return new SplittableStatusRecord(SplittableStatus.OK, project, o.get());
|
||||
} else {
|
||||
return new SplittableStatusRecord(SplittableStatus.OK, project, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void splitProject(long projectId) {
|
||||
SplittableStatusRecord result = getSplittableStatus(projectId);
|
||||
if (result.splittableStatus() != SplittableStatus.OK) {
|
||||
throw new IllegalStateException(
|
||||
"Project must to be verified to be able to split " + "before this method can be called."
|
||||
);
|
||||
}
|
||||
|
||||
Project project = result.project();
|
||||
|
||||
for (User author : project.getProjectParticipants()) {
|
||||
Project childProject = new Project();
|
||||
|
||||
childProject.setTitle(project.getTitle());
|
||||
childProject.setProjectType(project.getProjectType());
|
||||
childProject.setProjectStatus(ProjectStatus.ACTIVE);
|
||||
childProject.setResearchArea(project.getResearchArea());
|
||||
|
||||
childProject.setStartDate(project.getStartDate());
|
||||
childProject.setExpectedEndDate(project.getExpectedEndDate());
|
||||
|
||||
childProject.setHeadSupervisor(project.getHeadSupervisor());
|
||||
childProject.setProjectParticipants(List.of(author));
|
||||
childProject.setCoSupervisors(project.getCoSupervisors());
|
||||
|
||||
childProject.setReviewers(project.getReviewers());
|
||||
|
||||
childProject.setParentProjectId(project.getId());
|
||||
childProject.setRootProjectId(
|
||||
project.getRootProjectId() != null ? project.getRootProjectId() : project.getId()
|
||||
);
|
||||
childProject.setCloneTimestamp(Instant.now());
|
||||
|
||||
childProject = projectService.save(childProject);
|
||||
|
||||
// Add cloned RoughDraftApproval if it's 'APPROVED'
|
||||
RoughDraftApproval rda = result.roughDraftApproval();
|
||||
if (rda != null && rda.isApproved()) {
|
||||
RoughDraftApproval clonedRda = roughDraftApprovalService.saveCloned(rda.cloneToProject(childProject));
|
||||
|
||||
// Send event to eventBus to synchronize eventual Phase Two Approval with MileStone
|
||||
eventBus.post(new RoughDraftApprovalApprovedClonedEvent(clonedRda));
|
||||
}
|
||||
}
|
||||
|
||||
// Parent project will set as inactive
|
||||
project.setProjectStatus(ProjectStatus.INACTIVE);
|
||||
projectService.save(project);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public List<Project> getChildProjects(long parentProjectId) {
|
||||
return projectService.findAll(QProject.project.rootProjectId.eq(parentProjectId));
|
||||
}
|
||||
}
|
@ -196,6 +196,25 @@ public class Decision {
|
||||
decideWithDecisionDate(status, reason, attachment, Instant.now());
|
||||
}
|
||||
|
||||
Decision cloneToReviewerApproval(RoughDraftApproval roughDraftApproval) {
|
||||
Decision clonedDecision = new Decision();
|
||||
clonedDecision.reviewerApproval = roughDraftApproval;
|
||||
|
||||
clonedDecision.status = this.status;
|
||||
clonedDecision.reason = this.reason;
|
||||
clonedDecision.comment = this.comment;
|
||||
clonedDecision.requested = this.requested;
|
||||
clonedDecision.decisionDate = this.decisionDate;
|
||||
clonedDecision.deadline = this.deadline;
|
||||
clonedDecision.reviewerAssignedAt = this.reviewerAssignedAt;
|
||||
|
||||
clonedDecision.assignedReviewer = this.assignedReviewer;
|
||||
clonedDecision.attachment = this.attachment;
|
||||
clonedDecision.thesis = this.thesis;
|
||||
|
||||
return clonedDecision;
|
||||
}
|
||||
|
||||
private void decideWithDecisionDate(
|
||||
final Status status,
|
||||
final String reason,
|
||||
|
@ -25,6 +25,7 @@ public class DecisionRepositoryImpl extends AbstractRepository implements Decisi
|
||||
.eq(reviewer)
|
||||
.and(QDecision.decision.reviewerAssignedAt.goe(fromDate))
|
||||
.and(QDecision.decision.reviewerAssignedAt.loe(toDate))
|
||||
.and(QDecision.decision.reviewerApproval.isCloned.isFalse())
|
||||
)
|
||||
.distinct()
|
||||
.fetchCount();
|
||||
|
@ -1,6 +1,8 @@
|
||||
package se.su.dsv.scipro.reviewing;
|
||||
|
||||
import jakarta.persistence.Basic;
|
||||
import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.DiscriminatorColumn;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
@ -11,6 +13,7 @@ import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.OneToOne;
|
||||
import jakarta.persistence.OrderBy;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedList;
|
||||
@ -32,6 +35,14 @@ public abstract class ReviewerApproval extends DomainObject {
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Basic
|
||||
@Column(name = "is_cloned")
|
||||
protected Boolean isCloned = false;
|
||||
|
||||
@Basic
|
||||
@Column(name = "clone_timestamp")
|
||||
protected Instant cloneTimestamp;
|
||||
|
||||
// ----------------------------------------------------------------------------------
|
||||
tozh4728 marked this conversation as resolved
Outdated
ansv7779
commented
This should be a This should be a `java.time.Instant` not `java.util.Date`.
[`@Temporal` is deprecated](https://jakarta.ee/specifications/persistence/3.2/apidocs/jakarta.persistence/jakarta/persistence/temporal).
|
||||
// JPA-mappings of foreign keys in this table (reviewer_approval) referencing other
|
||||
// tables.
|
||||
@ -59,6 +70,22 @@ public abstract class ReviewerApproval extends DomainObject {
|
||||
return this.project;
|
||||
}
|
||||
|
||||
public Boolean getCloned() {
|
||||
return isCloned;
|
||||
}
|
||||
|
||||
public void setCloned(Boolean cloned) {
|
||||
isCloned = cloned;
|
||||
}
|
||||
|
||||
public Instant getCloneTimestamp() {
|
||||
return cloneTimestamp;
|
||||
}
|
||||
|
||||
public void setCloneTimestamp(Instant cloneDate) {
|
||||
this.cloneTimestamp = cloneDate;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------
|
||||
// Other methods
|
||||
// ----------------------------------------------------------------------------------
|
||||
|
@ -1,7 +1,8 @@
|
||||
package se.su.dsv.scipro.reviewing;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import java.util.*;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import se.su.dsv.scipro.file.FileReference;
|
||||
import se.su.dsv.scipro.project.Project;
|
||||
|
||||
@ -24,4 +25,15 @@ public class RoughDraftApproval extends ReviewerApproval {
|
||||
public Step getStep() {
|
||||
return Step.ROUGH_DRAFT_APPROVAL;
|
||||
}
|
||||
|
||||
public RoughDraftApproval cloneToProject(final Project newProject) {
|
||||
RoughDraftApproval rda = new RoughDraftApproval();
|
||||
rda.project = newProject;
|
||||
this.decisions.forEach(decision -> rda.decisions.add(decision.cloneToReviewerApproval(rda)));
|
||||
|
||||
rda.isCloned = true;
|
||||
rda.cloneTimestamp = Instant.now();
|
||||
ansv7779
commented
If possible try to get the current timestamp from an injected 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;
|
||||
}
|
||||
}
|
||||
|
14
core/src/main/java/se/su/dsv/scipro/reviewing/RoughDraftApprovalApprovedClonedEvent.java
Normal file
@ -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;
|
274
core/src/test/java/se/su/dsv/scipro/project/split/SplitOrRestartProjectServiceIntegrationTest.java
Normal file
@ -0,0 +1,274 @@
|
||||
package se.su.dsv.scipro.project.split;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static se.su.dsv.scipro.project.split.SplitOrRestartProjectService.SplittableStatus;
|
||||
import static se.su.dsv.scipro.project.split.SplitOrRestartProjectService.SplittableStatusRecord;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Year;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import se.su.dsv.scipro.file.FileUpload;
|
||||
import se.su.dsv.scipro.finalseminar.FinalSeminar;
|
||||
import se.su.dsv.scipro.project.Project;
|
||||
import se.su.dsv.scipro.project.ProjectStatus;
|
||||
import se.su.dsv.scipro.reviewing.*;
|
||||
import se.su.dsv.scipro.security.auth.roles.Roles;
|
||||
import se.su.dsv.scipro.system.*;
|
||||
import se.su.dsv.scipro.test.IntegrationTest;
|
||||
|
||||
public class SplitOrRestartProjectServiceIntegrationTest extends IntegrationTest {
|
||||
|
||||
private static int TARGET = 3;
|
||||
private static int REMAINING_TARGET = 2;
|
||||
|
||||
@Inject
|
||||
private SplitOrRestartProjectService splitOrRestartProjectService;
|
||||
|
||||
@Inject
|
||||
private RoughDraftApprovalService roughDraftApprovalService;
|
||||
|
||||
@Inject
|
||||
private FinalSeminarApprovalService finalSeminarApprovalService;
|
||||
|
||||
@Inject
|
||||
private ReviewerAssignmentService reviewerAssignmentService;
|
||||
|
||||
@Inject
|
||||
private ReviewerCapacityService reviewerCapacityService;
|
||||
|
||||
private ResearchArea researchArea;
|
||||
private Project parentProject;
|
||||
private User supervisor;
|
||||
private User reviewer;
|
||||
private User author1;
|
||||
private User author2;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
ProjectType bachelor = save(new ProjectType(DegreeType.BACHELOR, "Bachelor", "Bachelor"));
|
||||
|
||||
researchArea = new ResearchArea();
|
||||
researchArea.setTitle("Computer Science");
|
||||
researchArea = save(researchArea);
|
||||
|
||||
parentProject = createProject(bachelor, ProjectStatus.ACTIVE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void project_must_exist() {
|
||||
SplittableStatusRecord record = splitOrRestartProjectService.getSplittableStatus(0);
|
||||
SplittableStatus status = record.splittableStatus();
|
||||
|
||||
tozh4728 marked this conversation as resolved
ansv7779
commented
Use Use `assertEquals` instead of `assertTrue`. (This goes for every test).
|
||||
assertEquals(SplittableStatus.NOT_EXIST, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void project_must_be_active() {
|
||||
parentProject.setProjectStatus(ProjectStatus.INACTIVE);
|
||||
parentProject = save(parentProject);
|
||||
|
||||
SplittableStatusRecord record = splitOrRestartProjectService.getSplittableStatus(parentProject.getId());
|
||||
SplittableStatus status = record.splittableStatus();
|
||||
|
||||
assertEquals(SplittableStatus.NOT_ACTIVE, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void project_must_have_two_participants() {
|
||||
SplittableStatusRecord record = splitOrRestartProjectService.getSplittableStatus(parentProject.getId());
|
||||
SplittableStatus status = record.splittableStatus();
|
||||
|
||||
assertEquals(SplittableStatus.NOT_TWO_PARTICIPANTS, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void project_phase_two_started() {
|
||||
setUpBeforePhaseTwo();
|
||||
|
||||
SplittableStatusRecord record = splitOrRestartProjectService.getSplittableStatus(parentProject.getId());
|
||||
SplittableStatus status = record.splittableStatus();
|
||||
|
||||
assertEquals(SplittableStatus.PHASE_TWO_STARTED, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void split_on_failed_phase_two() {
|
||||
setUpBeforePhaseTwo();
|
||||
|
||||
Optional<RoughDraftApproval> optional = this.roughDraftApprovalService.findBy(parentProject);
|
||||
optional.ifPresent(rda -> rda.reject("Fail", Optional.empty()));
|
||||
|
||||
SplittableStatusRecord record = splitOrRestartProjectService.getSplittableStatus(parentProject.getId());
|
||||
SplittableStatus status = record.splittableStatus();
|
||||
|
||||
assertEquals(SplittableStatus.OK, status);
|
||||
|
||||
splitOrRestartProjectService.splitProject(parentProject.getId());
|
||||
|
||||
ReviewerCapacityService.RemainingTargets remainingTargets = reviewerCapacityService.getRemainingTargets(
|
||||
reviewer,
|
||||
Year.now()
|
||||
);
|
||||
ansv7779
commented
Integration tests have access to time manipulation via injecting an instance of Integration tests have access to time manipulation via injecting an instance of `MutableFixedClock`. This should allow you to set a specific date in either autumn or spring to get better assertions. Use `Year.now(clock)` to get the correct year for the fixed date that's been set.
|
||||
// Todo: improve this with higher precision by using MutabledFixedClock
|
||||
assertTrue(remainingTargets.spring() == REMAINING_TARGET || remainingTargets.autumn() == REMAINING_TARGET);
|
||||
|
||||
List<Project> childProjects = splitOrRestartProjectService.getChildProjects(parentProject.getId());
|
||||
|
||||
assertEquals(ProjectStatus.INACTIVE, parentProject.getProjectStatus());
|
||||
assertEquals(2, childProjects.size());
|
||||
|
||||
childProjects.forEach(project -> {
|
||||
assertEquals(1, project.getProjectParticipants().size());
|
||||
assertEquals(supervisor, project.getHeadSupervisor());
|
||||
assertEquals(ProjectStatus.ACTIVE, project.getProjectStatus());
|
||||
assertTrue(roughDraftApprovalService.findBy(project).isEmpty());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void split_on_approved_phase_two() {
|
||||
setUpBeforePhaseTwo();
|
||||
|
||||
Optional<RoughDraftApproval> optional = this.roughDraftApprovalService.findBy(parentProject);
|
||||
optional.ifPresent(rda -> rda.approve("Approve", Optional.empty()));
|
||||
|
||||
SplittableStatusRecord record = splitOrRestartProjectService.getSplittableStatus(parentProject.getId());
|
||||
SplittableStatus status = record.splittableStatus();
|
||||
|
||||
assertEquals(SplittableStatus.OK, status);
|
||||
|
||||
splitOrRestartProjectService.splitProject(parentProject.getId());
|
||||
|
||||
ReviewerCapacityService.RemainingTargets remainingTargets = reviewerCapacityService.getRemainingTargets(
|
||||
reviewer,
|
||||
Year.now()
|
||||
ansv7779
commented
Integration tests have access to time manipulation via injecting an instance of Integration tests have access to time manipulation via injecting an instance of `MutableFixedClock`. This should allow you to set a specific date in either autumn or spring to get better assertions. Use `Year.now(clock)` to get the correct year for the fixed date that's been set.
|
||||
);
|
||||
// todo: improve this by using MutableFixedClock
|
||||
assertTrue(remainingTargets.spring() == REMAINING_TARGET || remainingTargets.autumn() == REMAINING_TARGET);
|
||||
|
||||
List<Project> childProjects = splitOrRestartProjectService.getChildProjects(parentProject.getId());
|
||||
|
||||
assertEquals(ProjectStatus.INACTIVE, parentProject.getProjectStatus());
|
||||
assertEquals(2, childProjects.size());
|
||||
|
||||
childProjects.forEach(project -> {
|
||||
assertEquals(1, project.getProjectParticipants().size());
|
||||
assertEquals(supervisor, project.getHeadSupervisor());
|
||||
assertEquals(ProjectStatus.ACTIVE, project.getProjectStatus());
|
||||
|
||||
Optional<RoughDraftApproval> optionalRda = roughDraftApprovalService.findBy(project);
|
||||
assertTrue(optionalRda.isPresent());
|
||||
assertTrue(optional.get().isApproved());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void split_on_final_seminar_phase_started() {
|
||||
setUpBeforePhaseTwo();
|
||||
|
||||
Optional<RoughDraftApproval> optional = this.roughDraftApprovalService.findBy(parentProject);
|
||||
optional.ifPresent(rda -> rda.approve("Approve", Optional.empty()));
|
||||
|
||||
FinalSeminar finalSeminar = new FinalSeminar();
|
||||
finalSeminar.setProject(parentProject);
|
||||
finalSeminar.setStartDate(Date.from(ZonedDateTime.now().plusDays(10).toInstant()));
|
||||
finalSeminar.setRoom("room");
|
||||
finalSeminar.setPresentationLanguage(Language.ENGLISH);
|
||||
parentProject.setLanguage(Language.ENGLISH);
|
||||
|
||||
save(finalSeminar);
|
||||
|
||||
SplittableStatusRecord record = splitOrRestartProjectService.getSplittableStatus(parentProject.getId());
|
||||
SplittableStatus status = record.splittableStatus();
|
||||
|
||||
assertEquals(SplittableStatus.FINAL_SEMINAR_PHASE_STARTED, status);
|
||||
}
|
||||
|
||||
private Project createProject(ProjectType projectType, ProjectStatus active) {
|
||||
supervisor = createEmployee("Eric", "Employee", "eric@example.com", false);
|
||||
|
||||
Project project = new Project();
|
||||
project.setTitle("Some title");
|
||||
project.setProjectType(projectType);
|
||||
project.setProjectStatus(active);
|
||||
project.setHeadSupervisor(supervisor);
|
||||
project.setStartDate(LocalDate.now());
|
||||
|
||||
author1 = createUser("Adam", "Student", "adam.student@example.com");
|
||||
project.addProjectParticipant(author1);
|
||||
|
||||
project = save(project);
|
||||
return project;
|
||||
}
|
||||
|
||||
private User createEmployee(String firstName, String lastName, String email, boolean isReviewer) {
|
||||
User employee = createUser(firstName, lastName, email);
|
||||
|
||||
Set<ResearchArea> researchAreas = new HashSet<>();
|
||||
researchAreas.add(researchArea);
|
||||
employee.setResearchAreas(researchAreas);
|
||||
|
||||
if (isReviewer) {
|
||||
employee.addRole(Roles.REVIEWER);
|
||||
}
|
||||
|
||||
save(employee);
|
||||
return employee;
|
||||
}
|
||||
|
||||
private User createUser(String firstName, String lastName, String email) {
|
||||
User user = User.builder().firstName(firstName).lastName(lastName).emailAddress(email).build();
|
||||
return save(user);
|
||||
}
|
||||
|
||||
private void setUpBeforePhaseTwo() {
|
||||
author2 = createUser("Bertil", "Student", "bertil.student@example.com");
|
||||
parentProject.addProjectParticipant(author2);
|
||||
save(parentProject);
|
||||
|
||||
roughDraftApprovalService.requestApproval(parentProject, dummyFile(), "comment");
|
||||
|
||||
reviewer = createEmployee("Lisa", "Employee", "lisa.employee@example.com", true);
|
||||
reviewerCapacityService.assignTarget(
|
||||
reviewer,
|
||||
new ReviewerCapacityService.Target(Year.now(), TARGET, TARGET, "")
|
||||
);
|
||||
reviewerAssignmentService.assignReviewer(parentProject, reviewer);
|
||||
}
|
||||
|
||||
private FileUpload dummyFile() {
|
||||
return new FileUpload() {
|
||||
@Override
|
||||
public String getFileName() {
|
||||
return "dummy.tmp";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
return "text/plain";
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getUploader() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T handleData(Function<InputStream, T> handler) {
|
||||
return handler.apply(InputStream.nullInputStream());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -73,6 +73,9 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
|
||||
@Inject
|
||||
private MilestoneActivityTemplateService milestoneActivityTemplateService;
|
||||
|
||||
@Inject
|
||||
private EventService eventService;
|
||||
|
||||
@Inject
|
||||
private FileService fileService;
|
||||
|
||||
@ -2016,34 +2019,37 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
|
||||
MilestoneActivityTemplate.Type.STUDENT,
|
||||
"First meeting held",
|
||||
"First meeting with supervisor.",
|
||||
milestonePhaseTemplate1,
|
||||
null
|
||||
);
|
||||
createMileStone(
|
||||
MilestoneActivityTemplate.Type.PROJECT,
|
||||
"Project plan approved",
|
||||
"Project plan approved by supervisor.",
|
||||
milestonePhaseTemplate1,
|
||||
null
|
||||
milestonePhaseTemplate1
|
||||
);
|
||||
|
||||
createMileStone(
|
||||
MilestoneActivityTemplate.Type.PROJECT,
|
||||
"Rough draft sent to reviewer for approval",
|
||||
"Rough draft approved by reviewer.",
|
||||
milestonePhaseTemplate2,
|
||||
null
|
||||
"Project plan approved",
|
||||
"Project plan approved by supervisor.",
|
||||
milestonePhaseTemplate1
|
||||
);
|
||||
|
||||
List<Event> events = eventService.findAll();
|
||||
|
||||
createMileStone(
|
||||
MilestoneActivityTemplate.Type.PROJECT,
|
||||
"Rough draft approved by reviewer",
|
||||
"Rough draft sent to reviewer for approval (Auto)",
|
||||
"Rough draft sent to the reviewer for the first time.",
|
||||
milestonePhaseTemplate2,
|
||||
events.stream().filter(event -> event.getName().equals("RoughDraftApprovalRequested")).findFirst().get()
|
||||
);
|
||||
|
||||
createMileStone(
|
||||
MilestoneActivityTemplate.Type.PROJECT,
|
||||
"Rough draft approved by reviewer (Auto)",
|
||||
"Rough draft approved.",
|
||||
milestonePhaseTemplate2,
|
||||
null
|
||||
events.stream().filter(event -> event.getName().equals("Step.ROUGH_DRAFT_APPROVAL")).findFirst().get()
|
||||
);
|
||||
|
||||
createMileStone(
|
||||
MilestoneActivityTemplate.Type.STUDENT,
|
||||
"Peer review 1",
|
||||
"Peer review 1 (Auto)",
|
||||
"This is a recommendation of when to perform peer review 1.",
|
||||
milestonePhaseTemplate2,
|
||||
MilestoneActivityTemplate.PEER_REVIEW_ONE
|
||||
@ -2053,9 +2059,9 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
|
||||
MilestoneActivityTemplate.Type.PROJECT,
|
||||
"Result and discussion completed and approved",
|
||||
"Result and discussion.",
|
||||
milestonePhaseTemplate3,
|
||||
null
|
||||
milestonePhaseTemplate3
|
||||
);
|
||||
|
||||
createMileStone(
|
||||
MilestoneActivityTemplate.Type.STUDENT,
|
||||
"Peer review 2",
|
||||
@ -2068,9 +2074,9 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
|
||||
MilestoneActivityTemplate.Type.PROJECT,
|
||||
"Thesis approved for final seminar presentation",
|
||||
"Thesis approved for final seminar.",
|
||||
milestonePhaseTemplate4,
|
||||
null
|
||||
milestonePhaseTemplate4
|
||||
);
|
||||
|
||||
createMileStone(
|
||||
MilestoneActivityTemplate.Type.PROJECT,
|
||||
"Final seminar created",
|
||||
@ -2078,6 +2084,7 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
|
||||
milestonePhaseTemplate4,
|
||||
MilestoneActivityTemplate.CREATE_SEMINAR
|
||||
);
|
||||
|
||||
createMileStone(
|
||||
MilestoneActivityTemplate.Type.PROJECT,
|
||||
"Final seminar thesis uploaded",
|
||||
@ -2085,64 +2092,93 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
|
||||
milestonePhaseTemplate4,
|
||||
MilestoneActivityTemplate.THESIS_UPLOADED
|
||||
);
|
||||
|
||||
createMileStone(
|
||||
MilestoneActivityTemplate.Type.STUDENT,
|
||||
"Perform an oral and written opposition",
|
||||
"Opposition.",
|
||||
milestonePhaseTemplate4,
|
||||
null
|
||||
milestonePhaseTemplate4
|
||||
);
|
||||
|
||||
createMileStone(
|
||||
MilestoneActivityTemplate.Type.STUDENT,
|
||||
"Active participation in a final seminar",
|
||||
"Active participation.",
|
||||
milestonePhaseTemplate4,
|
||||
null
|
||||
milestonePhaseTemplate4
|
||||
);
|
||||
|
||||
createMileStone(
|
||||
MilestoneActivityTemplate.Type.STUDENT,
|
||||
"Defend the thesis in a final seminar",
|
||||
"Defence of final thesis.",
|
||||
milestonePhaseTemplate4,
|
||||
null
|
||||
milestonePhaseTemplate4
|
||||
);
|
||||
|
||||
createMileStone(
|
||||
MilestoneActivityTemplate.Type.PROJECT,
|
||||
"Revised final thesis of the submitted thesis",
|
||||
"Revised final thesis.",
|
||||
milestonePhaseTemplate5,
|
||||
null
|
||||
milestonePhaseTemplate5
|
||||
);
|
||||
|
||||
createMileStone(
|
||||
MilestoneActivityTemplate.Type.PROJECT,
|
||||
"Originality report approved",
|
||||
"Originality report.",
|
||||
milestonePhaseTemplate5,
|
||||
null
|
||||
milestonePhaseTemplate5
|
||||
);
|
||||
|
||||
createMileStone(
|
||||
MilestoneActivityTemplate.Type.PROJECT,
|
||||
"Supervisor and reviewer final grading report submitted",
|
||||
"Final grading report.",
|
||||
milestonePhaseTemplate5,
|
||||
null
|
||||
milestonePhaseTemplate5
|
||||
);
|
||||
|
||||
createMileStone(
|
||||
MilestoneActivityTemplate.Type.STUDENT,
|
||||
"Grading completed",
|
||||
"Grading completed by examiner.",
|
||||
milestonePhaseTemplate5,
|
||||
null
|
||||
milestonePhaseTemplate5
|
||||
);
|
||||
}
|
||||
|
||||
private void createMileStone(
|
||||
MilestoneActivityTemplate.Type type,
|
||||
String title,
|
||||
String description,
|
||||
MilestonePhaseTemplate milestonePhaseTemplate
|
||||
) {
|
||||
createMileStone(type, title, description, milestonePhaseTemplate, null, null);
|
||||
}
|
||||
|
||||
private void createMileStone(
|
||||
MilestoneActivityTemplate.Type type,
|
||||
String title,
|
||||
String description,
|
||||
MilestonePhaseTemplate milestonePhaseTemplate,
|
||||
Event event
|
||||
) {
|
||||
createMileStone(type, title, description, milestonePhaseTemplate, null, event);
|
||||
}
|
||||
|
||||
private void createMileStone(
|
||||
MilestoneActivityTemplate.Type type,
|
||||
String title,
|
||||
String description,
|
||||
MilestonePhaseTemplate milestonePhaseTemplate,
|
||||
String code
|
||||
tozh4728 marked this conversation as resolved
Outdated
ansv7779
commented
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 For the future it might be a good idea to add an overload to reduce the overall diff. It is also confusing to read method calls with many `null` parameters.
|
||||
) {
|
||||
createMileStone(type, title, description, milestonePhaseTemplate, code, null);
|
||||
}
|
||||
|
||||
private void createMileStone(
|
||||
MilestoneActivityTemplate.Type type,
|
||||
String title,
|
||||
String description,
|
||||
MilestonePhaseTemplate milestonePhaseTemplate,
|
||||
String code,
|
||||
Event event
|
||||
) {
|
||||
MilestoneActivityTemplate milestoneActivityTemplate = new MilestoneActivityTemplate(type, title, description);
|
||||
milestoneActivityTemplate.addProjectType(bachelorClass);
|
||||
@ -2150,6 +2186,7 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
|
||||
milestoneActivityTemplate.addProjectType(magisterClass);
|
||||
milestoneActivityTemplate.setMilestonePhaseTemplate(milestonePhaseTemplate);
|
||||
milestoneActivityTemplate.setCode(code);
|
||||
milestoneActivityTemplate.setActivatedBy(event);
|
||||
milestoneActivityTemplateService.save(milestoneActivityTemplate, milestonePhaseTemplate);
|
||||
}
|
||||
|
||||
|
102
test-data/src/main/java/se/su/dsv/scipro/testdata/populators/SplitProjectPopulator.java
vendored
Normal file
@ -0,0 +1,102 @@
|
||||
package se.su.dsv.scipro.testdata.populators;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Year;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import org.springframework.stereotype.Service;
|
||||
import se.su.dsv.scipro.file.FileUpload;
|
||||
import se.su.dsv.scipro.project.Project;
|
||||
import se.su.dsv.scipro.project.ProjectService;
|
||||
import se.su.dsv.scipro.reviewing.ReviewerAssignmentService;
|
||||
import se.su.dsv.scipro.reviewing.ReviewerCapacityService;
|
||||
import se.su.dsv.scipro.reviewing.RoughDraftApproval;
|
||||
import se.su.dsv.scipro.reviewing.RoughDraftApprovalService;
|
||||
import se.su.dsv.scipro.system.User;
|
||||
import se.su.dsv.scipro.testdata.BaseData;
|
||||
import se.su.dsv.scipro.testdata.Factory;
|
||||
import se.su.dsv.scipro.testdata.TestDataPopulator;
|
||||
|
||||
@Service
|
||||
public class SplitProjectPopulator implements TestDataPopulator {
|
||||
|
||||
private final ProjectService projectService;
|
||||
private final ReviewerCapacityService reviewerCapacityService;
|
||||
private final RoughDraftApprovalService roughDraftApprovalService;
|
||||
private final ReviewerAssignmentService reviewerAssignmentService;
|
||||
|
||||
@Inject
|
||||
public SplitProjectPopulator(
|
||||
ProjectService projectService,
|
||||
ReviewerCapacityService reviewerCapacityService,
|
||||
RoughDraftApprovalService roughDraftApprovalService,
|
||||
ReviewerAssignmentService reviewerAssignmentService
|
||||
) {
|
||||
this.projectService = projectService;
|
||||
this.reviewerCapacityService = reviewerCapacityService;
|
||||
this.roughDraftApprovalService = roughDraftApprovalService;
|
||||
this.reviewerAssignmentService = reviewerAssignmentService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void populate(BaseData baseData, Factory factory) {
|
||||
User supervisor = factory.createSupervisor("Emil");
|
||||
|
||||
User author1 = factory.createAuthor("Scott");
|
||||
User author2 = factory.createAuthor("Scarlett");
|
||||
|
||||
User reviewer = factory.createReviewer("Elias");
|
||||
|
||||
reviewerCapacityService.assignTarget(reviewer, new ReviewerCapacityService.Target(Year.now(), 3, 3, ""));
|
||||
|
||||
Project project = Project.builder()
|
||||
.title("Operating System Boot Time Security")
|
||||
.projectType(baseData.bachelor())
|
||||
.startDate(LocalDate.now())
|
||||
.headSupervisor(supervisor)
|
||||
.reviewers(Set.of(reviewer))
|
||||
.projectParticipants(Set.of(author1, author2))
|
||||
.build();
|
||||
|
||||
projectService.save(project);
|
||||
|
||||
roughDraftApprovalService.requestApproval(project, dummyFile(), "Request review.");
|
||||
|
||||
reviewerAssignmentService.assignReviewer(project, reviewer);
|
||||
|
||||
Optional<RoughDraftApproval> optional = roughDraftApprovalService.findBy(project);
|
||||
optional.ifPresent(rda -> rda.approve("Approve! Good Work!", Optional.empty()));
|
||||
}
|
||||
|
||||
private FileUpload dummyFile() {
|
||||
return new FileUpload() {
|
||||
@Override
|
||||
public String getFileName() {
|
||||
return "dummy.tmp";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
return "text/plain";
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getUploader() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T handleData(Function<InputStream, T> handler) {
|
||||
return handler.apply(InputStream.nullInputStream());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -294,6 +294,8 @@ public class SciProApplication extends LifecycleManagedWebApplication {
|
||||
mountPage("admin/maintenance", SystemMaintenancePage.class);
|
||||
mountPage("admin/project", ProjectManagementPage.class);
|
||||
mountPage("admin/project/create", AdminCreateProjectPage.class);
|
||||
mountPage("admin/project/split", AdminSplitProjectPage.class);
|
||||
mountPage("admin/project/viewparentproject", AdminViewParentProjectPage.class);
|
||||
mountPage("admin/project/survey", AdminSurveyPage.class);
|
||||
mountPage("admin/project/reviewer", AdminAssignReviewerPage.class);
|
||||
mountPage("admin/project/reviewer/capacity", AdminReviewerCapacityManagementPage.class);
|
||||
|
@ -49,6 +49,9 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<wicket:container wicket:id="splitPanel"/>
|
||||
|
||||
</wicket:extend>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -3,14 +3,28 @@ package se.su.dsv.scipro.admin.pages;
|
||||
import com.google.common.eventbus.EventBus;
|
||||
import jakarta.inject.Inject;
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import org.apache.wicket.RestartResponseException;
|
||||
import org.apache.wicket.markup.html.form.*;
|
||||
import org.apache.wicket.markup.html.form.DropDownChoice;
|
||||
import org.apache.wicket.markup.html.form.Form;
|
||||
import org.apache.wicket.markup.html.form.FormComponent;
|
||||
import org.apache.wicket.markup.html.form.LambdaChoiceRenderer;
|
||||
import org.apache.wicket.markup.html.form.RequiredTextField;
|
||||
import org.apache.wicket.markup.html.form.TextField;
|
||||
import org.apache.wicket.markup.html.panel.FeedbackPanel;
|
||||
import org.apache.wicket.model.IModel;
|
||||
import org.apache.wicket.model.LambdaModel;
|
||||
import org.apache.wicket.request.mapper.parameter.PageParameters;
|
||||
import se.su.dsv.scipro.components.*;
|
||||
import se.su.dsv.scipro.components.AutoCompleteRoleProvider;
|
||||
import se.su.dsv.scipro.components.BootstrapDatePicker;
|
||||
import se.su.dsv.scipro.components.DatesValidator;
|
||||
import se.su.dsv.scipro.components.DefaultSelect2MultiChoice;
|
||||
import se.su.dsv.scipro.components.ResearchAreaChoiceRenderer;
|
||||
import se.su.dsv.scipro.components.ResearchAreasModel;
|
||||
import se.su.dsv.scipro.components.SupervisorAutoComplete;
|
||||
import se.su.dsv.scipro.components.menuhighlighting.MenuHighlightAdminProjectManagement;
|
||||
import se.su.dsv.scipro.data.DetachableServiceModel;
|
||||
import se.su.dsv.scipro.data.DetachableServiceModelCollection;
|
||||
@ -26,7 +40,13 @@ import se.su.dsv.scipro.project.ProjectService;
|
||||
import se.su.dsv.scipro.project.ReviewerAssignedEvent;
|
||||
import se.su.dsv.scipro.security.auth.Authorization;
|
||||
import se.su.dsv.scipro.security.auth.roles.Roles;
|
||||
import se.su.dsv.scipro.system.*;
|
||||
import se.su.dsv.scipro.system.ProjectType;
|
||||
import se.su.dsv.scipro.system.ProjectTypeService;
|
||||
import se.su.dsv.scipro.system.ResearchArea;
|
||||
import se.su.dsv.scipro.system.ResearchAreaService;
|
||||
import se.su.dsv.scipro.system.User;
|
||||
import se.su.dsv.scipro.system.UserSearchService;
|
||||
import se.su.dsv.scipro.system.UserService;
|
||||
import se.su.dsv.scipro.util.PageParameterKeys;
|
||||
|
||||
@Authorization(authorizedRoles = { Roles.SYSADMIN })
|
||||
@ -74,7 +94,10 @@ public class AdminEditProjectPage extends AbstractAdminProjectPage implements Me
|
||||
if (project == null) {
|
||||
throw new RestartResponseException(AdminCreateProjectPage.class);
|
||||
}
|
||||
add(new ProjectForm(FORM, new DetachableServiceModel<>(projectService, project)));
|
||||
DetachableServiceModel<Project> dsModel = new DetachableServiceModel<>(projectService, project);
|
||||
|
||||
add(new ProjectForm(FORM, dsModel));
|
||||
add(new AdminSplitProjectPanel("splitPanel", dsModel));
|
||||
}
|
||||
|
||||
private class ProjectForm extends Form<Project> {
|
||||
|
@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org" lang="en">
|
||||
<body>
|
||||
<wicket:extend>
|
||||
<div class="row">
|
||||
<div class="col-lg-5">
|
||||
<h4 wicket:id="projectTitle"></h4>
|
||||
|
||||
<form class="form-horizontal" wicket:id="splitProjectForm">
|
||||
<p>This project will be split between following authors:</p>
|
||||
|
||||
<div class="mb-3">
|
||||
<ul wicket:id="authorList">
|
||||
<li wicket:id="author"></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" type="submit">Split Project</button>
|
||||
|
||||
<a wicket:id="cancelLink">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</wicket:extend>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,73 @@
|
||||
package se.su.dsv.scipro.admin.pages;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import java.util.ArrayList;
|
||||
import org.apache.wicket.RestartResponseException;
|
||||
import org.apache.wicket.markup.html.basic.Label;
|
||||
import org.apache.wicket.markup.html.form.Form;
|
||||
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
|
||||
import org.apache.wicket.markup.html.list.ListItem;
|
||||
import org.apache.wicket.markup.html.list.ListView;
|
||||
import org.apache.wicket.model.IModel;
|
||||
import org.apache.wicket.request.mapper.parameter.PageParameters;
|
||||
import se.su.dsv.scipro.components.menuhighlighting.MenuHighlightAdminProjectManagement;
|
||||
import se.su.dsv.scipro.data.DetachableServiceModel;
|
||||
import se.su.dsv.scipro.project.Project;
|
||||
import se.su.dsv.scipro.project.ProjectService;
|
||||
import se.su.dsv.scipro.project.split.SplitOrRestartProjectService;
|
||||
import se.su.dsv.scipro.security.auth.Authorization;
|
||||
import se.su.dsv.scipro.security.auth.roles.Roles;
|
||||
import se.su.dsv.scipro.system.User;
|
||||
import se.su.dsv.scipro.util.PageParameterKeys;
|
||||
|
||||
@Authorization(authorizedRoles = { Roles.SYSADMIN })
|
||||
public class AdminSplitProjectPage extends AbstractAdminProjectPage implements MenuHighlightAdminProjectManagement {
|
||||
|
||||
@Inject
|
||||
private ProjectService projectService;
|
||||
|
||||
@Inject
|
||||
private SplitOrRestartProjectService splitOrRestartProjectService;
|
||||
|
||||
public AdminSplitProjectPage(PageParameters pp) {
|
||||
final long id = pp.get(PageParameterKeys.MAP.get(Project.class)).toLong(0);
|
||||
final Project project = projectService.findOne(id);
|
||||
if (project == null) {
|
||||
throw new RestartResponseException(AdminCreateProjectPage.class);
|
||||
}
|
||||
DetachableServiceModel<Project> dsModel = new DetachableServiceModel<>(projectService, project);
|
||||
|
||||
add(new Label("projectTitle", dsModel.map(Project::getTitle)));
|
||||
|
||||
add(new SplitProjectForm("splitProjectForm", dsModel));
|
||||
}
|
||||
|
||||
private class SplitProjectForm extends Form<Project> {
|
||||
|
||||
public SplitProjectForm(String id, final IModel<Project> model) {
|
||||
super(id, model);
|
||||
add(
|
||||
new ListView<>("authorList", model.map(Project::getProjectParticipants).map(ArrayList::new)) {
|
||||
@Override
|
||||
protected void populateItem(ListItem<User> item) {
|
||||
item.add(new Label("author", item.getModel().map(User::getFullName)));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
add(new BookmarkablePageLink<Void>("cancelLink", ProjectManagementPage.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSubmit() {
|
||||
Long projectId = getModel().getObject().getId();
|
||||
|
||||
splitOrRestartProjectService.splitProject(projectId);
|
||||
|
||||
final PageParameters pp = new PageParameters();
|
||||
pp.set(PageParameterKeys.MAP.get(Project.class), projectId);
|
||||
|
||||
setResponsePage(AdminViewParentProjectPage.class, pp);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
|
||||
<body>
|
||||
<wicket:panel>
|
||||
<div class="row mt-5">
|
||||
<div class="col-md-12 col-lg-8 col-xl-5">
|
||||
<p>
|
||||
<a class="btn" wicket:id="splitProjectLink"><wicket:message key="splitButton" /></a>
|
||||
</p>
|
||||
<p wicket:id="splitInfo"></p>
|
||||
</div>
|
||||
</div>
|
||||
</wicket:panel>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,78 @@
|
||||
package se.su.dsv.scipro.admin.pages;
|
||||
|
||||
import static se.su.dsv.scipro.project.split.SplitOrRestartProjectService.SplittableStatus;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import org.apache.wicket.behavior.AttributeAppender;
|
||||
import org.apache.wicket.markup.html.basic.Label;
|
||||
import org.apache.wicket.markup.html.link.AbstractLink;
|
||||
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
|
||||
import org.apache.wicket.markup.html.panel.Panel;
|
||||
import org.apache.wicket.model.IModel;
|
||||
import org.apache.wicket.model.LoadableDetachableModel;
|
||||
import org.apache.wicket.model.Model;
|
||||
import org.apache.wicket.request.mapper.parameter.PageParameters;
|
||||
import se.su.dsv.scipro.project.Project;
|
||||
import se.su.dsv.scipro.project.split.SplitOrRestartProjectService;
|
||||
import se.su.dsv.scipro.security.auth.roles.Roles;
|
||||
import se.su.dsv.scipro.session.SciProSession;
|
||||
import se.su.dsv.scipro.util.PageParameterKeys;
|
||||
|
||||
public class AdminSplitProjectPanel extends Panel {
|
||||
|
||||
@Inject
|
||||
private SplitOrRestartProjectService splitOrRestartProjectService;
|
||||
|
||||
public AdminSplitProjectPanel(String id, final IModel<Project> projectModel) {
|
||||
super(id, projectModel);
|
||||
Project project = projectModel.getObject();
|
||||
|
||||
LoadableDetachableModel<SplittableStatus> ldModel = LoadableDetachableModel.of(() -> {
|
||||
SplitOrRestartProjectService.SplittableStatusRecord status =
|
||||
splitOrRestartProjectService.getSplittableStatus(project.getId());
|
||||
return status.splittableStatus();
|
||||
});
|
||||
|
||||
final PageParameters pp = new PageParameters();
|
||||
pp.set(PageParameterKeys.MAP.get(Project.class), project.getId());
|
||||
|
||||
AbstractLink splitProjectLink = new BookmarkablePageLink<Void>(
|
||||
"splitProjectLink",
|
||||
AdminSplitProjectPage.class,
|
||||
pp
|
||||
) {
|
||||
@Override
|
||||
protected void onConfigure() {
|
||||
super.onConfigure();
|
||||
|
||||
if (ldModel.getObject() == SplittableStatus.OK) {
|
||||
setEnabled(true);
|
||||
this.add(new AttributeAppender("class", Model.of(" btn-success")));
|
||||
} else {
|
||||
setEnabled(false);
|
||||
this.add(new AttributeAppender("class", Model.of(" btn-secondary disabled")));
|
||||
}
|
||||
}
|
||||
};
|
||||
add(splitProjectLink);
|
||||
|
||||
add(new Label("splitInfo", Model.of(getStatusMessage(ldModel.getObject()))));
|
||||
ansv7779
commented
Replace with Otherwise I'm impressed with how well you've handled the 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");
|
||||
};
|
||||
}
|
||||
}
|
6
view/src/main/java/se/su/dsv/scipro/admin/pages/AdminSplitProjectPanel.utf8.properties
Normal file
@ -0,0 +1,6 @@
|
||||
splitButton = Split Project
|
||||
status_not_exit = Project does not exist.
|
||||
status_not_active = Only active project can be split.
|
||||
status_not_two_participants = To be able to split a project, it needs to have 2 participants.
|
||||
status_phase_two_started = Phase 2 (Review) is already started, can't split right now.
|
||||
status_final_seminar_phase_started = Final seminar phase has been started, too late to split.
|
@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org" lang="en">
|
||||
<body>
|
||||
<wicket:extend>
|
||||
<div class="row">
|
||||
<div class="col-lg-5">
|
||||
<h4 wicket:id="projectTitle"></h4>
|
||||
|
||||
<p>The project has following children projects:</p>
|
||||
|
||||
<div class="mb-3">
|
||||
<ul wicket:id="projectList">
|
||||
<li><a wicket:id="editLink"></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<a class="btn btn-success" wicket:id="link">Projects</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</wicket:extend>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,68 @@
|
||||
package se.su.dsv.scipro.admin.pages;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import java.util.List;
|
||||
import org.apache.wicket.RestartResponseException;
|
||||
import org.apache.wicket.markup.html.basic.Label;
|
||||
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
|
||||
import org.apache.wicket.markup.html.list.ListItem;
|
||||
import org.apache.wicket.markup.html.list.ListView;
|
||||
import org.apache.wicket.model.LoadableDetachableModel;
|
||||
import org.apache.wicket.model.Model;
|
||||
import org.apache.wicket.request.mapper.parameter.PageParameters;
|
||||
import se.su.dsv.scipro.components.menuhighlighting.MenuHighlightAdminProjectManagement;
|
||||
import se.su.dsv.scipro.project.Project;
|
||||
import se.su.dsv.scipro.project.ProjectService;
|
||||
import se.su.dsv.scipro.project.split.SplitOrRestartProjectService;
|
||||
import se.su.dsv.scipro.security.auth.Authorization;
|
||||
import se.su.dsv.scipro.security.auth.roles.Roles;
|
||||
import se.su.dsv.scipro.util.PageParameterKeys;
|
||||
|
||||
@Authorization(authorizedRoles = { Roles.SYSADMIN })
|
||||
public class AdminViewParentProjectPage
|
||||
extends AbstractAdminProjectPage
|
||||
implements MenuHighlightAdminProjectManagement {
|
||||
|
||||
@Inject
|
||||
private ProjectService projectService;
|
||||
|
||||
@Inject
|
||||
private SplitOrRestartProjectService splitOrRestartProjectService;
|
||||
|
||||
public AdminViewParentProjectPage(PageParameters pp) {
|
||||
final long id = pp.get(PageParameterKeys.MAP.get(Project.class)).toLong(0);
|
||||
final Project project = projectService.findOne(id);
|
||||
if (project == null) {
|
||||
throw new RestartResponseException(AdminCreateProjectPage.class);
|
||||
}
|
||||
|
||||
add(new Label("projectTitle", Model.of(project.getTitle())));
|
||||
|
||||
LoadableDetachableModel<List<Project>> ldModel = LoadableDetachableModel.of(() ->
|
||||
splitOrRestartProjectService.getChildProjects(id)
|
||||
);
|
||||
|
||||
add(
|
||||
new ListView<>("projectList", ldModel) {
|
||||
@Override
|
||||
protected void populateItem(ListItem<Project> item) {
|
||||
Project project = item.getModelObject();
|
||||
|
||||
final PageParameters pp = new PageParameters();
|
||||
pp.set(PageParameterKeys.MAP.get(Project.class), project.getId());
|
||||
|
||||
BookmarkablePageLink<Void> link = new BookmarkablePageLink<Void>(
|
||||
"editLink",
|
||||
AdminEditProjectPage.class,
|
||||
pp
|
||||
);
|
||||
link.setBody(Model.of(project.getTitle() + " - " + project.getAuthorNames()));
|
||||
ansv7779
commented
Should use Should use `item.getModel().map(project -> ...)` again to prevent possibility of stale data.
|
||||
|
||||
item.add(link);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
add(new BookmarkablePageLink<Void>("link", ProjectManagementPage.class));
|
||||
}
|
||||
}
|
@ -97,6 +97,7 @@ import se.su.dsv.scipro.project.ProjectNoteService;
|
||||
import se.su.dsv.scipro.project.ProjectPeopleStatisticsService;
|
||||
import se.su.dsv.scipro.project.ProjectService;
|
||||
import se.su.dsv.scipro.project.pages.ProjectStartPage;
|
||||
import se.su.dsv.scipro.project.split.SplitOrRestartProjectService;
|
||||
import se.su.dsv.scipro.projectpartner.ProjectPartnerService;
|
||||
import se.su.dsv.scipro.reflection.ReflectionService;
|
||||
import se.su.dsv.scipro.report.GradeCalculatorService;
|
||||
@ -252,6 +253,9 @@ public abstract class SciProTest {
|
||||
@Mock
|
||||
protected ProjectService projectService;
|
||||
|
||||
@Mock
|
||||
protected SplitOrRestartProjectService splitOrRestartProjectService;
|
||||
|
||||
@Mock
|
||||
protected ResearchAreaService researchAreaService;
|
||||
|
||||
|
@ -20,11 +20,13 @@ import se.su.dsv.scipro.mail.MailEvent;
|
||||
import se.su.dsv.scipro.notifications.dataobject.NotificationSource;
|
||||
import se.su.dsv.scipro.notifications.dataobject.ProjectEvent;
|
||||
import se.su.dsv.scipro.project.Project;
|
||||
import se.su.dsv.scipro.project.split.SplitOrRestartProjectService;
|
||||
import se.su.dsv.scipro.security.auth.roles.Roles;
|
||||
import se.su.dsv.scipro.system.DegreeType;
|
||||
import se.su.dsv.scipro.system.ProjectType;
|
||||
import se.su.dsv.scipro.system.User;
|
||||
import se.su.dsv.scipro.util.PageParameterKeys;
|
||||
import se.su.dsv.scipro.util.Pair;
|
||||
|
||||
public class AdminEditProjectPageTest extends SciProTest {
|
||||
|
||||
@ -269,6 +271,16 @@ public class AdminEditProjectPageTest extends SciProTest {
|
||||
|
||||
private void startPage(Project project) {
|
||||
if (project.getId() != null) when(projectService.findOne(project.getId())).thenReturn(project);
|
||||
lenient()
|
||||
.when(splitOrRestartProjectService.getSplittableStatus(project.getId() != null ? project.getId() : 0L))
|
||||
.thenReturn(
|
||||
new SplitOrRestartProjectService.SplittableStatusRecord(
|
||||
SplitOrRestartProjectService.SplittableStatus.OK,
|
||||
project,
|
||||
null
|
||||
)
|
||||
);
|
||||
|
||||
PageParameters pp = new PageParameters();
|
||||
pp.set(PageParameterKeys.MAP.get(Project.class), project.getId());
|
||||
tester.startPage(AdminEditProjectPage.class, pp);
|
||||
|
This should be a
java.time.Instant
notjava.util.Date
.@Temporal
is deprecated.