diff --git a/core/src/main/java/se/su/dsv/scipro/CoreConfig.java b/core/src/main/java/se/su/dsv/scipro/CoreConfig.java index 43c4d7ed10..080ff17d1a 100644 --- a/core/src/main/java/se/su/dsv/scipro/CoreConfig.java +++ b/core/src/main/java/se/su/dsv/scipro/CoreConfig.java @@ -31,6 +31,7 @@ import se.su.dsv.scipro.finalseminar.AuthorRepository; import se.su.dsv.scipro.finalseminar.FinalSeminarActiveParticipationRepository; import se.su.dsv.scipro.finalseminar.FinalSeminarActiveParticipationServiceImpl; import se.su.dsv.scipro.finalseminar.FinalSeminarCreationSubscribers; +import se.su.dsv.scipro.finalseminar.FinalSeminarOppositionGrading; import se.su.dsv.scipro.finalseminar.FinalSeminarOppositionRepo; import se.su.dsv.scipro.finalseminar.FinalSeminarOppositionServiceImpl; import se.su.dsv.scipro.finalseminar.FinalSeminarRepository; @@ -153,6 +154,7 @@ import se.su.dsv.scipro.report.GradingReportServiceImpl; import se.su.dsv.scipro.report.GradingReportTemplateRepo; import se.su.dsv.scipro.report.GradingReportTemplateRepoImpl; import se.su.dsv.scipro.report.OppositionReportRepo; +import se.su.dsv.scipro.report.OppositionReportService; import se.su.dsv.scipro.report.OppositionReportServiceImpl; import se.su.dsv.scipro.report.ReportServiceImpl; import se.su.dsv.scipro.report.SupervisorGradingReportRepository; @@ -430,8 +432,26 @@ public class CoreConfig { } @Bean - public FinalSeminarOppositionServiceImpl finalSeminarOppositionService(Provider em) { - return new FinalSeminarOppositionServiceImpl(em); + public FinalSeminarOppositionServiceImpl finalSeminarOppositionService( + Provider em, + FinalSeminarOppositionGrading finalSeminarOppositionGrading, + EventBus eventBus, + FinalSeminarOppositionRepo finalSeminarOppositionRepository, + Clock clock, + FinalSeminarSettingsService finalSeminarSettingsService, + DaysService daysService, + OppositionReportService oppositionReportService + ) { + return new FinalSeminarOppositionServiceImpl( + em, + finalSeminarOppositionGrading, + eventBus, + finalSeminarOppositionRepository, + clock, + finalSeminarSettingsService, + daysService, + oppositionReportService + ); } @Bean diff --git a/core/src/main/java/se/su/dsv/scipro/DataInitializer.java b/core/src/main/java/se/su/dsv/scipro/DataInitializer.java index 7ab805cd7a..98ebc08e63 100644 --- a/core/src/main/java/se/su/dsv/scipro/DataInitializer.java +++ b/core/src/main/java/se/su/dsv/scipro/DataInitializer.java @@ -4,11 +4,20 @@ import jakarta.inject.Inject; import jakarta.inject.Provider; 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.util.*; +import java.util.function.Function; import se.su.dsv.scipro.checklist.ChecklistCategory; +import se.su.dsv.scipro.file.FileReference; +import se.su.dsv.scipro.file.FileService; +import se.su.dsv.scipro.file.FileUpload; +import se.su.dsv.scipro.finalseminar.FinalSeminar; +import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition; import se.su.dsv.scipro.match.ApplicationPeriod; import se.su.dsv.scipro.match.Keyword; import se.su.dsv.scipro.milestones.dataobjects.MilestoneActivityTemplate; @@ -39,6 +48,9 @@ public class DataInitializer implements Lifecycle { @Inject private MilestoneActivityTemplateService milestoneActivityTemplateService; + @Inject + private FileService fileService; + @Inject private CurrentProfile profile; @@ -75,6 +87,8 @@ public class DataInitializer implements Lifecycle { private ResearchArea researchArea2; private ProjectType masterClass; private ProjectType magisterClass; + private Project project1; + private Project project2; @Transactional @Override @@ -89,12 +103,35 @@ public class DataInitializer implements Lifecycle { createMilestonesIfNotDone(); createUsers(); createProjects(); + createPastFinalSeminar(); } if (profile.getCurrentProfile() == Profiles.DEV && noAdminUser()) { createAdmin(); } } + private void createPastFinalSeminar() { + FileReference document = fileService.storeFile( + new SimpleTextFile(sture_student, "document.txt", "Hello World") + ); + + FinalSeminar finalSeminar = new FinalSeminar(); + finalSeminar.setStartDate(Date.from(ZonedDateTime.now().minusDays(1).toInstant())); + finalSeminar.setProject(project1); + finalSeminar.setRoom("zoom"); + finalSeminar.setPresentationLanguage(Language.ENGLISH); + finalSeminar.setDocument(document); + finalSeminar.setDocumentUploadDate(document.getFileDescription().getDateCreated()); + + FinalSeminarOpposition opponent = new FinalSeminarOpposition(); + opponent.setProject(project2); + opponent.setFinalSeminar(finalSeminar); + opponent.setUser(sid_student); + finalSeminar.addOpposition(opponent); + + save(finalSeminar); + } + @Override public void stop() {} @@ -145,11 +182,11 @@ public class DataInitializer implements Lifecycle { } private void createProjects() { - createProject(PROJECT_1, eric_employee, sture_student, stina_student, eve_employee); - createProject(PROJECT_2, eve_employee, sid_student, simon_student, eric_employee); + project1 = createProject(PROJECT_1, eric_employee, sture_student, stina_student, eve_employee); + project2 = createProject(PROJECT_2, eve_employee, sid_student, simon_student, eric_employee); } - private void createProject(String title, User headSupervisor, User student1, User student2, User reviewer) { + private Project createProject(String title, User headSupervisor, User student1, User student2, User reviewer) { Project project = Project.builder() .title(title) .projectType(bachelorClass) @@ -159,7 +196,7 @@ public class DataInitializer implements Lifecycle { project.addProjectParticipant(student2); project.addProjectParticipant(student1); project.addReviewer(reviewer); - save(project); + return save(project); } private void createUsers() { @@ -1902,4 +1939,42 @@ public class DataInitializer implements Lifecycle { em.get().persist(entity); return entity; } + + private static final class SimpleTextFile implements FileUpload { + + private final User uploader; + private final String fileName; + private final String content; + + private SimpleTextFile(User uploader, String fileName, String content) { + this.uploader = uploader; + this.fileName = fileName; + this.content = content; + } + + @Override + public String getFileName() { + return fileName; + } + + @Override + public String getContentType() { + return "text/plain"; + } + + @Override + public User getUploader() { + return uploader; + } + + @Override + public long getSize() { + return content.length(); + } + + @Override + public T handleData(Function handler) { + return handler.apply(new ByteArrayInputStream(content.getBytes())); + } + } } diff --git a/core/src/main/java/se/su/dsv/scipro/finalseminar/AbstractOppositionEvent.java b/core/src/main/java/se/su/dsv/scipro/finalseminar/AbstractOppositionEvent.java index e60a7b2bc7..2143479147 100644 --- a/core/src/main/java/se/su/dsv/scipro/finalseminar/AbstractOppositionEvent.java +++ b/core/src/main/java/se/su/dsv/scipro/finalseminar/AbstractOppositionEvent.java @@ -1,5 +1,6 @@ package se.su.dsv.scipro.finalseminar; +import java.util.Objects; import se.su.dsv.scipro.project.Project; import se.su.dsv.scipro.system.User; @@ -26,4 +27,17 @@ class AbstractOppositionEvent { public FinalSeminarOpposition getOpposition() { return opposition; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AbstractOppositionEvent that = (AbstractOppositionEvent) o; + return Objects.equals(opposition, that.opposition); + } + + @Override + public int hashCode() { + return Objects.hashCode(opposition); + } } diff --git a/core/src/main/java/se/su/dsv/scipro/finalseminar/ExpireUnfulfilledOppositionImprovementsWorker.java b/core/src/main/java/se/su/dsv/scipro/finalseminar/ExpireUnfulfilledOppositionImprovementsWorker.java new file mode 100644 index 0000000000..9a92f0c18b --- /dev/null +++ b/core/src/main/java/se/su/dsv/scipro/finalseminar/ExpireUnfulfilledOppositionImprovementsWorker.java @@ -0,0 +1,32 @@ +package se.su.dsv.scipro.finalseminar; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import java.util.concurrent.TimeUnit; +import se.su.dsv.scipro.workerthreads.AbstractWorker; +import se.su.dsv.scipro.workerthreads.Scheduler; + +public class ExpireUnfulfilledOppositionImprovementsWorker extends AbstractWorker { + + private final FinalSeminarOppositionServiceImpl oppositionService; + + public ExpireUnfulfilledOppositionImprovementsWorker(FinalSeminarOppositionServiceImpl oppositionService) { + this.oppositionService = oppositionService; + } + + @Override + protected void doWork() { + oppositionService.expireUnfulfilledOppositionImprovements(); + } + + public static class Schedule { + + @Inject + public Schedule(Scheduler scheduler, Provider worker) { + scheduler + .schedule("Fail opponents that have not submitted improvements") + .runBy(worker) + .every(1, TimeUnit.HOURS); + } + } +} diff --git a/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOpposition.java b/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOpposition.java index 9825e981a8..8a1533fedd 100755 --- a/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOpposition.java +++ b/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOpposition.java @@ -8,6 +8,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToOne; import jakarta.persistence.Table; +import java.time.Instant; import java.util.Objects; import se.su.dsv.scipro.file.FileReference; import se.su.dsv.scipro.project.Project; @@ -31,6 +32,14 @@ public class FinalSeminarOpposition extends FinalSeminarParticipation { @Column(name = "feedback", length = FEEDBACK_LENGTH) private String feedback; + @Basic + @Column(name = "improvements_requested_at") + private Instant improvementsRequestedAt; + + @Basic + @Column(name = "supervisor_improvements_comment") + private String supervisorCommentForImprovements; + // ---------------------------------------------------------------------------------- // JPA-mappings of foreign keys in this table (final_seminar_opposition) referencing // other tables. @@ -92,6 +101,22 @@ public class FinalSeminarOpposition extends FinalSeminarParticipation { this.oppositionReport = oppositionReport; } + public Instant getImprovementsRequestedAt() { + return improvementsRequestedAt; + } + + public void setImprovementsRequestedAt(Instant improvementsRequestedAt) { + this.improvementsRequestedAt = improvementsRequestedAt; + } + + public String getSupervisorCommentForImprovements() { + return supervisorCommentForImprovements; + } + + public void setSupervisorCommentForImprovements(String supervisorCommentsForImprovements) { + this.supervisorCommentForImprovements = supervisorCommentsForImprovements; + } + // ---------------------------------------------------------------------------------- // Methods Common To All Objects // ---------------------------------------------------------------------------------- diff --git a/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionGrading.java b/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionGrading.java new file mode 100644 index 0000000000..2962d1b25e --- /dev/null +++ b/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionGrading.java @@ -0,0 +1,7 @@ +package se.su.dsv.scipro.finalseminar; + +import java.util.List; + +public interface FinalSeminarOppositionGrading { + OppositionCriteria oppositionCriteria(FinalSeminarOpposition opposition); +} diff --git a/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionRepo.java b/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionRepo.java index e636597075..b6e7a348c5 100755 --- a/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionRepo.java +++ b/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionRepo.java @@ -11,4 +11,6 @@ import se.su.dsv.scipro.system.User; public interface FinalSeminarOppositionRepo extends JpaRepository, QueryDslPredicateExecutor { List findByOpposingUserAndType(User user, ProjectType projectType); + + Collection findUnfulfilledOppositionImprovements(); } diff --git a/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionRepoImpl.java b/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionRepoImpl.java index 00ad589634..a48da4e198 100644 --- a/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionRepoImpl.java +++ b/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionRepoImpl.java @@ -24,4 +24,13 @@ public class FinalSeminarOppositionRepoImpl .where(QFinalSeminarOpposition.finalSeminarOpposition.project.projectType.eq(projectType)) .fetch(); } + + @Override + public Collection findUnfulfilledOppositionImprovements() { + return createQuery() + .innerJoin(QFinalSeminarOpposition.finalSeminarOpposition.oppositionReport) + .where(QFinalSeminarOpposition.finalSeminarOpposition.improvementsRequestedAt.isNotNull()) + .where(QFinalSeminarOpposition.finalSeminarOpposition.oppositionReport.submitted.isFalse()) + .fetch(); + } } diff --git a/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionService.java b/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionService.java index 60eeacc1a9..e7d7916583 100755 --- a/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionService.java +++ b/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionService.java @@ -1,8 +1,18 @@ package se.su.dsv.scipro.finalseminar; +import java.time.Instant; import se.su.dsv.scipro.system.GenericService; -public interface FinalSeminarOppositionService extends GenericService { - @Override - void delete(Long aLong); +public interface FinalSeminarOppositionService { + OppositionCriteria getCriteriaForOpposition(FinalSeminarOpposition opposition); + + FinalSeminarOpposition gradeOpponent(FinalSeminarOpposition opposition, int points, String feedback) + throws PointNotValidException; + + /** + * @return the deadline by which the improvements must have been submitted + */ + Instant requestImprovements(FinalSeminarOpposition opposition, String supervisorComment); + + Opposition getOpposition(long id); } diff --git a/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionServiceImpl.java b/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionServiceImpl.java index 27550bb3ac..1de2066727 100755 --- a/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionServiceImpl.java +++ b/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionServiceImpl.java @@ -1,16 +1,177 @@ package se.su.dsv.scipro.finalseminar; +import com.google.common.eventbus.EventBus; import jakarta.inject.Inject; import jakarta.inject.Provider; import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import java.time.Clock; +import java.time.Instant; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import se.su.dsv.scipro.misc.DaysService; +import se.su.dsv.scipro.report.OppositionReport; +import se.su.dsv.scipro.report.OppositionReportService; import se.su.dsv.scipro.system.AbstractServiceImpl; public class FinalSeminarOppositionServiceImpl extends AbstractServiceImpl implements FinalSeminarOppositionService { + private final FinalSeminarOppositionGrading finalSeminarOppositionGrading; + private final EventBus eventBus; + private final FinalSeminarOppositionRepo finalSeminarOppositionRepository; + private final Clock clock; + private final FinalSeminarSettingsService finalSeminarSettingsService; + private final DaysService daysService; + private final OppositionReportService oppositionReportService; + @Inject - public FinalSeminarOppositionServiceImpl(Provider em) { + public FinalSeminarOppositionServiceImpl( + Provider em, + FinalSeminarOppositionGrading finalSeminarOppositionGrading, + EventBus eventBus, + FinalSeminarOppositionRepo finalSeminarOppositionRepository, + Clock clock, + FinalSeminarSettingsService finalSeminarSettingsService, + DaysService daysService, + OppositionReportService oppositionReportService + ) { super(em, FinalSeminarOpposition.class, QFinalSeminarOpposition.finalSeminarOpposition); + this.finalSeminarOppositionGrading = finalSeminarOppositionGrading; + this.eventBus = eventBus; + this.finalSeminarOppositionRepository = finalSeminarOppositionRepository; + this.clock = clock; + this.finalSeminarSettingsService = finalSeminarSettingsService; + this.daysService = daysService; + this.oppositionReportService = oppositionReportService; + } + + @Override + public OppositionCriteria getCriteriaForOpposition(FinalSeminarOpposition opposition) { + return finalSeminarOppositionGrading.oppositionCriteria(opposition); + } + + @Override + @Transactional + public FinalSeminarOpposition gradeOpponent(FinalSeminarOpposition opposition, int points, String feedback) + throws PointNotValidException { + OppositionCriteria criteriaForOpposition = getCriteriaForOpposition(opposition); + boolean validPoints = criteriaForOpposition + .pointsAvailable() + .stream() + .anyMatch(criterion -> criterion.value() == points); + if (!validPoints) { + throw new PointNotValidException(points, List.of(0, 1)); + } + + FinalSeminarGrade notApproved = criteriaForOpposition.pointsToPass() > points + ? FinalSeminarGrade.NOT_APPROVED + : FinalSeminarGrade.APPROVED; + return internalGradeOpponent(opposition, points, feedback, notApproved); + } + + private FinalSeminarOpposition internalGradeOpponent( + FinalSeminarOpposition opposition, + int points, + String feedback, + FinalSeminarGrade grade + ) { + opposition.setGrade(grade); + opposition.setPoints(points); + opposition.setFeedback(feedback); + FinalSeminarOpposition assessedOpposition = finalSeminarOppositionRepository.save(opposition); + + if (grade == FinalSeminarGrade.NOT_APPROVED) { + eventBus.post(new OppositionFailedEvent(assessedOpposition)); + } else { + eventBus.post(new OppositionApprovedEvent(assessedOpposition)); + } + + return assessedOpposition; + } + + @Override + @Transactional + public Instant requestImprovements(FinalSeminarOpposition opposition, String supervisorComment) { + OppositionReport oppositionReport = opposition.getOppositionReport(); + if (oppositionReport == null) { + throw new IllegalStateException("There is no opposition report submitted"); + } + + FinalSeminarSettings finalSeminarSettings = finalSeminarSettingsService.getInstance(); + + Instant now = clock.instant(); + Instant deadline = daysService.workDaysAfter( + now, + finalSeminarSettings.getWorkDaysToFixRequestedImprovementsToOppositionReport() + ); + + oppositionReport.setSubmitted(false); + opposition.setImprovementsRequestedAt(now); + opposition.setSupervisorCommentForImprovements(supervisorComment); + + eventBus.post(new OppositionReportImprovementsRequestedEvent(opposition, supervisorComment, deadline)); + + return deadline; + } + + @Override + public Opposition getOpposition(long id) { + FinalSeminarOpposition finalSeminarOpposition = finalSeminarOppositionRepository.findOne(id); + if (finalSeminarOpposition == null) { + return null; + } + OppositionReport report = oppositionReportService.findOrCreateReport(finalSeminarOpposition); + Optional improvements = getImprovementsNeeded(finalSeminarOpposition); + return new Opposition(finalSeminarOpposition.getUser(), report, improvements); + } + + private Optional getImprovementsNeeded( + FinalSeminarOpposition finalSeminarOpposition + ) { + if (finalSeminarOpposition.getSupervisorCommentForImprovements() != null) { + return Optional.of( + new Opposition.ImprovementsNeeded( + finalSeminarOpposition.getSupervisorCommentForImprovements(), + finalSeminarOpposition.getImprovementsRequestedAt() + ) + ); + } else { + return Optional.empty(); + } + } + + void expireUnfulfilledOppositionImprovements() { + Collection unfulfilledOppositions = + finalSeminarOppositionRepository.findUnfulfilledOppositionImprovements(); + + Instant now = clock.instant(); + int workDaysToFixRequestedImprovementsToOppositionReport = finalSeminarSettingsService + .getInstance() + .getWorkDaysToFixRequestedImprovementsToOppositionReport(); + for (FinalSeminarOpposition unfulfilledOpposition : unfulfilledOppositions) { + Instant deadline = daysService.workDaysAfter( + unfulfilledOpposition.getImprovementsRequestedAt(), + workDaysToFixRequestedImprovementsToOppositionReport + ); + if (now.isAfter(deadline)) { + internalGradeOpponent( + unfulfilledOpposition, + 0, + unfulfilledOpposition.getSupervisorCommentForImprovements(), + FinalSeminarGrade.NOT_APPROVED + ); + + OppositionReport oppositionReport = unfulfilledOpposition.getOppositionReport(); + if (oppositionReport != null) { + // Lock the report so it's not possible to submit it again + oppositionReport.setSubmitted(true); + } + + finalSeminarOppositionRepository.save(unfulfilledOpposition); + } + } } } diff --git a/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarServiceImpl.java b/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarServiceImpl.java index 0af91dca55..02892489a0 100755 --- a/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarServiceImpl.java +++ b/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarServiceImpl.java @@ -2,7 +2,10 @@ package se.su.dsv.scipro.finalseminar; import com.google.common.eventbus.EventBus; import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.SubQueryExpressionImpl; import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.JPAExpressions; import jakarta.inject.Inject; import jakarta.inject.Provider; import jakarta.persistence.EntityManager; @@ -542,19 +545,22 @@ public class FinalSeminarServiceImpl extends AbstractServiceImpl improvementsNeeded) { + record ImprovementsNeeded(String comment, Instant deadline) {} +} diff --git a/core/src/main/java/se/su/dsv/scipro/finalseminar/OppositionCriteria.java b/core/src/main/java/se/su/dsv/scipro/finalseminar/OppositionCriteria.java new file mode 100644 index 0000000000..96be8aa6a1 --- /dev/null +++ b/core/src/main/java/se/su/dsv/scipro/finalseminar/OppositionCriteria.java @@ -0,0 +1,7 @@ +package se.su.dsv.scipro.finalseminar; + +import java.util.List; + +public record OppositionCriteria(int pointsToPass, List pointsAvailable) { + public record Point(int value, String requirement) {} +} diff --git a/core/src/main/java/se/su/dsv/scipro/finalseminar/OppositionReportImprovementsRequestedEvent.java b/core/src/main/java/se/su/dsv/scipro/finalseminar/OppositionReportImprovementsRequestedEvent.java new file mode 100644 index 0000000000..0c063e49cf --- /dev/null +++ b/core/src/main/java/se/su/dsv/scipro/finalseminar/OppositionReportImprovementsRequestedEvent.java @@ -0,0 +1,9 @@ +package se.su.dsv.scipro.finalseminar; + +import java.time.Instant; + +public record OppositionReportImprovementsRequestedEvent( + FinalSeminarOpposition opposition, + String supervisorComment, + Instant deadline +) {} diff --git a/core/src/main/java/se/su/dsv/scipro/finalseminar/PointNotValidException.java b/core/src/main/java/se/su/dsv/scipro/finalseminar/PointNotValidException.java new file mode 100644 index 0000000000..82e92fc68f --- /dev/null +++ b/core/src/main/java/se/su/dsv/scipro/finalseminar/PointNotValidException.java @@ -0,0 +1,27 @@ +package se.su.dsv.scipro.finalseminar; + +import java.util.List; + +public class PointNotValidException extends Exception { + + private final int givenValue; + private final List acceptableValues; + + public PointNotValidException(int givenValue, List acceptableValues) { + this.givenValue = givenValue; + this.acceptableValues = acceptableValues; + } + + public int givenValue() { + return givenValue; + } + + public List acceptableValues() { + return acceptableValues; + } + + @Override + public String toString() { + return "PointNotValidException{" + "givenValue=" + givenValue + ", acceptableValues=" + acceptableValues + '}'; + } +} diff --git a/core/src/main/java/se/su/dsv/scipro/misc/DaysService.java b/core/src/main/java/se/su/dsv/scipro/misc/DaysService.java index d52c0e467e..865d495bf7 100644 --- a/core/src/main/java/se/su/dsv/scipro/misc/DaysService.java +++ b/core/src/main/java/se/su/dsv/scipro/misc/DaysService.java @@ -1,6 +1,9 @@ package se.su.dsv.scipro.misc; +import java.time.Instant; import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Date; public interface DaysService { @@ -9,4 +12,11 @@ public interface DaysService { int workDaysBetween(Date startDate, Date endDate); LocalDate workDaysAhead(LocalDate date, int days); LocalDate workDaysAfter(LocalDate date, int days); + + default Instant workDaysAfter(Instant instant, int days) { + ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault()); + LocalDate localDate = zonedDateTime.toLocalDate(); + LocalDate newDate = workDaysAfter(localDate, days); + return newDate.atTime(zonedDateTime.toLocalTime()).atZone(ZoneId.systemDefault()).toInstant(); + } } diff --git a/core/src/main/java/se/su/dsv/scipro/notifications/Notifications.java b/core/src/main/java/se/su/dsv/scipro/notifications/Notifications.java index 00cbe6b0da..abf4fcb159 100644 --- a/core/src/main/java/se/su/dsv/scipro/notifications/Notifications.java +++ b/core/src/main/java/se/su/dsv/scipro/notifications/Notifications.java @@ -12,6 +12,7 @@ import se.su.dsv.scipro.finalseminar.FinalSeminarDeletedEvent; import se.su.dsv.scipro.finalseminar.FinalSeminarThesisDeletedEvent; import se.su.dsv.scipro.finalseminar.FinalSeminarThesisUploadedEvent; import se.su.dsv.scipro.finalseminar.OppositionFailedEvent; +import se.su.dsv.scipro.finalseminar.OppositionReportImprovementsRequestedEvent; import se.su.dsv.scipro.finalseminar.ParticipationFailedEvent; import se.su.dsv.scipro.notifications.dataobject.NotificationSource; import se.su.dsv.scipro.notifications.dataobject.PeerEvent; @@ -168,6 +169,20 @@ public class Notifications { ); } + @Subscribe + public void oppositionReportImprovementsRequested(OppositionReportImprovementsRequestedEvent event) { + Member recipient = new Member(event.opposition().getUser(), Member.Type.AUTHOR); + Set recipients = Set.of(recipient); + NotificationSource source = new NotificationSource(); + source.setMessage(event.supervisorComment()); + notificationController.notifyCustomProject( + event.opposition().getProject(), + ProjectEvent.Event.OPPOSITION_REPORT_IMPROVEMENTS_REQUESTED, + source, + recipients + ); + } + @Subscribe public void reviewersChanged(ReviewerAssignedEvent event) { notificationController.notifyProject( diff --git a/core/src/main/java/se/su/dsv/scipro/notifications/dataobject/ProjectEvent.java b/core/src/main/java/se/su/dsv/scipro/notifications/dataobject/ProjectEvent.java index 99fa21978a..0e0352631c 100755 --- a/core/src/main/java/se/su/dsv/scipro/notifications/dataobject/ProjectEvent.java +++ b/core/src/main/java/se/su/dsv/scipro/notifications/dataobject/ProjectEvent.java @@ -51,6 +51,7 @@ public class ProjectEvent extends NotificationEvent { PARTICIPATION_FAILED, REFLECTION_IMPROVEMENTS_REQUESTED, REFLECTION_IMPROVEMENTS_SUBMITTED, + OPPOSITION_REPORT_IMPROVEMENTS_REQUESTED, } @Basic diff --git a/core/src/main/java/se/su/dsv/scipro/notifications/notifications.properties b/core/src/main/java/se/su/dsv/scipro/notifications/notifications.properties index c5cb11b76a..9019703177 100755 --- a/core/src/main/java/se/su/dsv/scipro/notifications/notifications.properties +++ b/core/src/main/java/se/su/dsv/scipro/notifications/notifications.properties @@ -79,6 +79,10 @@ PROJECT.FIRST_MEETING.body = Date: {0}\n\nDescription:\n{2} PROJECT.OPPOSITION_FAILED.title = Your opposition on {1} did not meet the minimum requirements. PROJECT.OPPOSITION_FAILED.body = Your opposition did not meet the minimum requirements set, and you will have to \ oppose on a different final seminar to pass this step.\n\nFeedback from the seminar supervisor: {2} +PROJECT.OPPOSITION_REPORT_IMPROVEMENTS_REQUESTED.title = Opposition report improvements requested +PROJECT.OPPOSITION_REPORT_IMPROVEMENTS_REQUESTED.body = The supervisor has deemed that the opposition report submitted \ + does not meet the minimum requirements and has requested improvements. Please log into SciPro and submit a new \ + opposition report. Their comments can be seen below:\n\n{0} PROJECT.PARTICIPATION_APPROVED.title = Active participation on {1} has been approved. PROJECT.PARTICIPATION_APPROVED.body = Your active participation on {0} has been approved, but you still have to complete \ {2} more active participation to meet the minimum requirements for your thesis project. diff --git a/core/src/main/java/se/su/dsv/scipro/report/GradingReportService.java b/core/src/main/java/se/su/dsv/scipro/report/GradingReportService.java index 1c38af9b44..409bf6385d 100644 --- a/core/src/main/java/se/su/dsv/scipro/report/GradingReportService.java +++ b/core/src/main/java/se/su/dsv/scipro/report/GradingReportService.java @@ -2,7 +2,6 @@ package se.su.dsv.scipro.report; import java.time.Instant; import java.util.List; -import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition; import se.su.dsv.scipro.grading.GradingBasis; import se.su.dsv.scipro.project.Project; import se.su.dsv.scipro.system.User; @@ -19,8 +18,6 @@ public interface GradingReportService { SupervisorGradingReport supervisorGradingReport ); - boolean updateOppositionCriteria(SupervisorGradingReport report, FinalSeminarOpposition opposition); - GradingBasis getGradingBasis(Project project); GradingBasis updateGradingBasis(Project project, GradingBasis gradingBasis); diff --git a/core/src/main/java/se/su/dsv/scipro/report/GradingReportServiceImpl.java b/core/src/main/java/se/su/dsv/scipro/report/GradingReportServiceImpl.java index bc945b6443..1d39c7306a 100644 --- a/core/src/main/java/se/su/dsv/scipro/report/GradingReportServiceImpl.java +++ b/core/src/main/java/se/su/dsv/scipro/report/GradingReportServiceImpl.java @@ -1,6 +1,7 @@ package se.su.dsv.scipro.report; import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; import jakarta.inject.Inject; import jakarta.transaction.Transactional; import java.time.Clock; @@ -8,6 +9,9 @@ import java.time.Instant; import java.time.LocalDate; import java.util.*; import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition; +import se.su.dsv.scipro.finalseminar.FinalSeminarOppositionGrading; +import se.su.dsv.scipro.finalseminar.OppositionApprovedEvent; +import se.su.dsv.scipro.finalseminar.OppositionCriteria; import se.su.dsv.scipro.grading.GradingBasis; import se.su.dsv.scipro.grading.GradingReportTemplateService; import se.su.dsv.scipro.grading.GradingReportTemplateUpdate; @@ -20,7 +24,8 @@ import se.su.dsv.scipro.system.ProjectTypeService; import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.util.Either; -public class GradingReportServiceImpl implements GradingReportTemplateService, GradingReportService { +public class GradingReportServiceImpl + implements GradingReportTemplateService, GradingReportService, FinalSeminarOppositionGrading { private final EventBus eventBus; private final ThesisSubmissionHistoryService thesisSubmissionHistoryService; @@ -44,11 +49,11 @@ public class GradingReportServiceImpl implements GradingReportTemplateService, G this.supervisorGradingReportRepository = supervisorGradingReportRepository; this.gradingReportTemplateRepo = gradingReportTemplateRepo; this.projectTypeService = projectTypeService; + + eventBus.register(this); } - @Override - @Transactional - public boolean updateOppositionCriteria(SupervisorGradingReport report, FinalSeminarOpposition opposition) { + private boolean updateOppositionCriteria(SupervisorGradingReport report, FinalSeminarOpposition opposition) { for (GradingCriterion gradingCriterion : report.getIndividualCriteria()) { boolean isOppositionCriterion = gradingCriterion.getFlag() == GradingCriterion.Flag.OPPOSITION; boolean betterGrade = @@ -289,4 +294,39 @@ public class GradingReportServiceImpl implements GradingReportTemplateService, G return gradingReportTemplateRepo.createTemplate(projectType, update); } + + @Subscribe + public void opponentApproved(OppositionApprovedEvent event) { + SupervisorGradingReport report = getSupervisorGradingReport(event.getProject(), event.getStudent()); + updateOppositionCriteria(report, event.getOpposition()); + } + + @Override + @Transactional + public OppositionCriteria oppositionCriteria(FinalSeminarOpposition opposition) { + SupervisorGradingReport supervisorGradingReport = getSupervisorGradingReport( + opposition.getProject(), + opposition.getUser() + ); + Optional oppositionGradingCriteria = supervisorGradingReport + .getIndividualCriteria() + .stream() + .filter(individualCriterion -> individualCriterion.getFlag() == AbstractGradingCriterion.Flag.OPPOSITION) + .findAny(); + if (oppositionGradingCriteria.isEmpty()) { + return new OppositionCriteria(0, List.of()); + } + List points = oppositionGradingCriteria + .stream() + .map(GradingCriterion::getGradingCriterionPoints) + .flatMap(Collection::stream) + .map(gcp -> + new OppositionCriteria.Point( + gcp.getPoint(), + Objects.requireNonNullElse(gcp.getDescription(Language.ENGLISH), "") + ) + ) + .toList(); + return new OppositionCriteria(oppositionGradingCriteria.get().getPointsRequiredToPass(), points); + } } diff --git a/core/src/main/resources/db/migration/V5__final_seminar_opposition_request_improvements.sql b/core/src/main/resources/db/migration/V5__final_seminar_opposition_request_improvements.sql new file mode 100644 index 0000000000..e922687232 --- /dev/null +++ b/core/src/main/resources/db/migration/V5__final_seminar_opposition_request_improvements.sql @@ -0,0 +1,3 @@ +ALTER TABLE `final_seminar_opposition` + ADD COLUMN `improvements_requested_at` DATETIME NULL, + ADD COLUMN `supervisor_improvements_comment` TEXT NULL; diff --git a/core/src/main/resources/db/migration/V6__final_seminar_work_days_to_fix_opposition_report.sql b/core/src/main/resources/db/migration/V6__final_seminar_work_days_to_fix_opposition_report.sql new file mode 100644 index 0000000000..f102c8fe14 --- /dev/null +++ b/core/src/main/resources/db/migration/V6__final_seminar_work_days_to_fix_opposition_report.sql @@ -0,0 +1,2 @@ +ALTER TABLE `final_seminar_settings` + ADD COLUMN `work_days_to_fix_requested_improvements_to_opposition_report` INT(11) NOT NULL DEFAULT 10; diff --git a/core/src/test/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionServiceImplIntegrationTest.java b/core/src/test/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionServiceImplIntegrationTest.java index 9b27323fd4..fb39050f2d 100644 --- a/core/src/test/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionServiceImplIntegrationTest.java +++ b/core/src/test/java/se/su/dsv/scipro/finalseminar/FinalSeminarOppositionServiceImplIntegrationTest.java @@ -1,14 +1,22 @@ package se.su.dsv.scipro.finalseminar; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import jakarta.inject.Inject; import java.time.LocalDate; import java.time.Month; import java.util.Date; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import se.su.dsv.scipro.project.Project; +import se.su.dsv.scipro.report.AbstractGradingCriterion; +import se.su.dsv.scipro.report.GradingCriterionPointTemplate; import se.su.dsv.scipro.report.GradingReportTemplate; import se.su.dsv.scipro.report.OppositionReport; import se.su.dsv.scipro.system.DegreeType; @@ -46,6 +54,76 @@ public class FinalSeminarOppositionServiceImplIntegrationTest extends Integratio assertEquals(0, finalSeminarOppositionService.count()); } + @Test + public void opposition_criteria_are_taken_from_the_grading_report_template() { + FinalSeminarOpposition opposition = createOpposition(finalSeminar, createUser()); + createSimpleGradingReportTemplateWithPassFail(); + + assertEquals(2, finalSeminarOppositionService.getCriteriaForOpposition(opposition).pointsAvailable().size()); + } + + @Test + public void can_not_grade_outside_criterion() { + FinalSeminarOpposition opposition = createOpposition(finalSeminar, createUser()); + createSimpleGradingReportTemplateWithPassFail(); + + PointNotValidException exception = assertThrows(PointNotValidException.class, () -> + finalSeminarOppositionService.gradeOpponent(opposition, 2, "Feedback") + ); + assertEquals(2, exception.givenValue()); + assertThat(exception.acceptableValues(), hasSize(2)); + assertThat(exception.acceptableValues(), contains(0, 1)); + } + + @Test + public void publishes_failed_event_when_grading_with_failing_criterion() throws Exception { + FinalSeminarOpposition opposition = createOpposition(finalSeminar, createUser()); + createSimpleGradingReportTemplateWithPassFail(); + + finalSeminarOppositionService.gradeOpponent(opposition, 0, "Feedback"); + + assertThat(getPublishedEvents(), hasItem(new OppositionFailedEvent(opposition))); + } + + @Test + public void publishes_approved_event_when_grading_with_passing_criterion() throws Exception { + FinalSeminarOpposition opposition = createOpposition(finalSeminar, createUser()); + createSimpleGradingReportTemplateWithPassFail(); + + finalSeminarOppositionService.gradeOpponent(opposition, 1, "Feedback"); + + assertThat(getPublishedEvents(), hasItem(new OppositionApprovedEvent(opposition))); + } + + @Test + public void stores_assessment() throws Exception { + FinalSeminarOpposition opposition = createOpposition(finalSeminar, createUser()); + createSimpleGradingReportTemplateWithPassFail(); + + FinalSeminarOpposition graded = finalSeminarOppositionService.gradeOpponent(opposition, 1, "Feedback"); + + assertEquals(1, graded.getPoints()); + assertEquals("Feedback", graded.getFeedback()); + } + + private void createSimpleGradingReportTemplateWithPassFail() { + GradingReportTemplate gradingReportTemplate = createGradingReportTemplate(); + + GradingCriterionPointTemplate failingCriterion = new GradingCriterionPointTemplate(); + failingCriterion.setPoint(0); + GradingCriterionPointTemplate passingCriterion = new GradingCriterionPointTemplate(); + passingCriterion.setPoint(1); + + gradingReportTemplate.addIndividualCriterion( + "Criterion 1", + "Criterion 1", + 1, + List.of(failingCriterion, passingCriterion), + AbstractGradingCriterion.Flag.OPPOSITION + ); + save(gradingReportTemplate); + } + private void createOppositionReport(FinalSeminarOpposition opposition) { OppositionReport report = new OppositionReport(createGradingReportTemplate(), opposition); opposition.setOppositionReport(report); @@ -93,7 +171,7 @@ public class FinalSeminarOppositionServiceImplIntegrationTest extends Integratio FinalSeminarOpposition opposition = new FinalSeminarOpposition(); opposition.setFinalSeminar(finalSeminar); opposition.setUser(student); - opposition.setProject(createProject(createProjectType())); + opposition.setProject(createProject(projectType)); return save(opposition); } } diff --git a/core/src/test/java/se/su/dsv/scipro/finalseminar/FinalSeminarServiceImplIntegrationTest.java b/core/src/test/java/se/su/dsv/scipro/finalseminar/FinalSeminarServiceImplIntegrationTest.java index 3cced9804a..9d86984f17 100644 --- a/core/src/test/java/se/su/dsv/scipro/finalseminar/FinalSeminarServiceImplIntegrationTest.java +++ b/core/src/test/java/se/su/dsv/scipro/finalseminar/FinalSeminarServiceImplIntegrationTest.java @@ -8,6 +8,7 @@ import static se.su.dsv.scipro.test.Matchers.isRight; import com.google.common.collect.Lists; import jakarta.inject.Inject; +import java.time.Duration; import java.time.LocalDate; import java.time.Month; import java.time.ZonedDateTime; @@ -30,6 +31,9 @@ public class FinalSeminarServiceImplIntegrationTest extends IntegrationTest { @Inject private FinalSeminarService finalSeminarService; + @Inject + private FinalSeminarOppositionService finalSeminarOppositionService; + private ProjectType projectType; private FinalSeminar futureFinalSeminar; private User user; @@ -309,6 +313,43 @@ public class FinalSeminarServiceImplIntegrationTest extends IntegrationTest { assertThat(finalSeminarService.canOppose(user, finalSeminar, otherProject), isRight(anything())); } + @Test + public void seminar_is_not_unfinished_if_opponent_has_improvements_requested() { + FinalSeminar seminar = createFinalSeminar(createProject(), -6); + FinalSeminarOpposition finalSeminarOpposition = addOpposition(seminar, null); + addOppositionReport(finalSeminarOpposition); + + Date after = Date.from(seminar.getStartDate().toInstant().minus(Duration.ofDays(1))); + Date before = Date.from(seminar.getStartDate().toInstant().plus(Duration.ofDays(1))); + + List unfinishedSeminars = finalSeminarService.findUnfinishedSeminars( + after, + before, + new PageRequest(0, 5) + ); + + assertThat(unfinishedSeminars, hasItem(seminar)); + + finalSeminarOppositionService.requestImprovements(finalSeminarOpposition, "improvements"); + + List afterImprovements = finalSeminarService.findUnfinishedSeminars( + after, + before, + new PageRequest(0, 5) + ); + + assertThat(afterImprovements, not(hasItem(seminar))); + } + + private static void addOppositionReport(FinalSeminarOpposition finalSeminarOpposition) { + GradingReportTemplate gradingReportTemplate = new GradingReportTemplate( + finalSeminarOpposition.getProjectType(), + LocalDate.now() + ); + OppositionReport oppositionReport = new OppositionReport(gradingReportTemplate, finalSeminarOpposition); + finalSeminarOpposition.setOppositionReport(oppositionReport); + } + private FinalSeminar createFutureFinalSeminarSomeDaysAgo(final int daysAgo) { FinalSeminar finalSeminar = initFinalSeminar(createProject(), 5); final Date dateCreated = Date.from(ZonedDateTime.now().minusDays(daysAgo).toInstant()); @@ -340,10 +381,10 @@ public class FinalSeminarServiceImplIntegrationTest extends IntegrationTest { save(seminar); } - private void addOpposition(FinalSeminar seminar, FinalSeminarGrade grade) { + private FinalSeminarOpposition addOpposition(FinalSeminar seminar, FinalSeminarGrade grade) { FinalSeminarOpposition opposition = createOpposition(seminar); opposition.setGrade(grade); - save(opposition); + return save(opposition); } private OppositionReport createOppositionReport(FinalSeminarOpposition opposition) { diff --git a/core/src/test/java/se/su/dsv/scipro/report/GradingReportServiceImplIntegrationTest.java b/core/src/test/java/se/su/dsv/scipro/report/GradingReportServiceImplIntegrationTest.java index 4eec189c37..597396bbea 100644 --- a/core/src/test/java/se/su/dsv/scipro/report/GradingReportServiceImplIntegrationTest.java +++ b/core/src/test/java/se/su/dsv/scipro/report/GradingReportServiceImplIntegrationTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.google.common.eventbus.EventBus; import jakarta.inject.Inject; import java.time.LocalDate; import java.time.Month; @@ -13,6 +14,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import se.su.dsv.scipro.finalseminar.FinalSeminar; import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition; +import se.su.dsv.scipro.finalseminar.OppositionApprovedEvent; import se.su.dsv.scipro.project.Project; import se.su.dsv.scipro.security.auth.roles.Roles; import se.su.dsv.scipro.system.DegreeType; @@ -31,6 +33,9 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest { @Inject private GradingReportServiceImpl gradingReportService; + @Inject + private EventBus eventBus; + private ProjectType projectType; private GradingReportTemplate gradingReportTemplate; private Project project; @@ -45,7 +50,6 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest { project = createProject(projectType, 30); gradingReportTemplate = createProjectGradingCriterion(gradingReportTemplate, 2); gradingReportTemplate = createIndividualGradingCriterion(gradingReportTemplate, 2); - gradingReport = createGradingReport(project, student); } @Test @@ -68,6 +72,7 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest { @Test public void submit_supervisor_grading_report_flags_report_as_submitted() { + gradingReport = createGradingReport(project, student); assessAllCriteria(gradingReport); Either, SupervisorGradingReport> result = gradingReportService.submitReport( gradingReport @@ -77,6 +82,7 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest { @Test public void submitting_supervisor_report_throws_exception_if_report_is_not_finished() { + gradingReport = createGradingReport(project, student); Either, SupervisorGradingReport> result = gradingReportService.submitReport( gradingReport ); @@ -86,38 +92,35 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest { @Test public void update_opposition_criterion() { addOppositionCriterion(); - boolean updated = updateOppositionCriterion(); + updateOppositionCriterion(); GradingCriterion oppositionCriterion = findOppositionCriterion(); assert oppositionCriterion != null; assertEquals(FEEDBACK_ON_OPPOSITION, oppositionCriterion.getFeedback()); assertEquals((Integer) OPPOSITION_CRITERION_POINTS, oppositionCriterion.getPoints()); - assertTrue(updated); } @Test public void update_opposition_if_title_matches_english_title() { addOppositionCriterion(); - boolean updated = updateOppositionCriterion(); + updateOppositionCriterion(); GradingCriterion oppositionCriterion = findEnglishOppositionCriterion("Ö1 Opposition report"); assert oppositionCriterion != null; assertEquals(FEEDBACK_ON_OPPOSITION, oppositionCriterion.getFeedback()); assertEquals((Integer) OPPOSITION_CRITERION_POINTS, oppositionCriterion.getPoints()); - assertTrue(updated); } @Test public void updating_opposition_criterion_does_nothing_if_criterion_already_has_values() { addOppositionCriterion(); assessAllCriteria(gradingReport); - boolean updated = updateOppositionCriterion(); + updateOppositionCriterion(); GradingCriterion oppositionCriterion = findOppositionCriterion(); assert oppositionCriterion != null; assertEquals(FEEDBACK, oppositionCriterion.getFeedback()); assertEquals((Integer) oppositionCriterion.getMaxPoints(), oppositionCriterion.getPoints()); - assertFalse(updated); } @Test @@ -151,9 +154,9 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest { gradingReport = createGradingReport(project, student); } - private boolean updateOppositionCriterion() { + private void updateOppositionCriterion() { FinalSeminarOpposition opposition = createFinalSeminarOpposition(); - return gradingReportService.updateOppositionCriteria(gradingReport, opposition); + eventBus.post(new OppositionApprovedEvent(opposition)); } private GradingCriterion findOppositionCriterion() { @@ -176,8 +179,8 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest { private FinalSeminarOpposition createFinalSeminarOpposition() { FinalSeminarOpposition finalSeminarOpposition = new FinalSeminarOpposition(); - finalSeminarOpposition.setProject(createProject(projectType, 30)); - finalSeminarOpposition.setUser(createStudent()); + finalSeminarOpposition.setProject(project); + finalSeminarOpposition.setUser(student); finalSeminarOpposition.setFinalSeminar(createFinalSeminar()); finalSeminarOpposition.setFeedback(FEEDBACK_ON_OPPOSITION); diff --git a/core/src/test/java/se/su/dsv/scipro/test/SpringTest.java b/core/src/test/java/se/su/dsv/scipro/test/SpringTest.java index 04d0f70da7..2d52bd0d36 100644 --- a/core/src/test/java/se/su/dsv/scipro/test/SpringTest.java +++ b/core/src/test/java/se/su/dsv/scipro/test/SpringTest.java @@ -1,11 +1,14 @@ package se.su.dsv.scipro.test; +import com.google.common.eventbus.EventBus; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.EntityTransaction; import jakarta.persistence.Persistence; import java.sql.SQLException; import java.time.Clock; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Optional; import org.flywaydb.core.Flyway; @@ -35,6 +38,8 @@ public abstract class SpringTest { @Container static MariaDBContainer mariaDBContainer = new MariaDBContainer<>("mariadb:10.11"); + private CapturingEventBus capturingEventBus; + @BeforeEach public final void prepareSpring() throws SQLException { MariaDbDataSource dataSource = new MariaDbDataSource(mariaDBContainer.getJdbcUrl()); @@ -50,8 +55,11 @@ public abstract class SpringTest { transaction.begin(); transaction.setRollbackOnly(); + capturingEventBus = new CapturingEventBus(); + AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(); + annotationConfigApplicationContext.registerBean("eventBus", EventBus.class, () -> this.capturingEventBus); annotationConfigApplicationContext.register(TestContext.class); annotationConfigApplicationContext.getBeanFactory().registerSingleton("entityManager", this.entityManager); annotationConfigApplicationContext.refresh(); @@ -75,6 +83,10 @@ public abstract class SpringTest { } } + protected List getPublishedEvents() { + return capturingEventBus.publishedEvents; + } + @Configuration(proxyBeanMethods = false) @Import({ CoreConfig.class, RepositoryConfiguration.class }) public static class TestContext { @@ -106,4 +118,15 @@ public abstract class SpringTest { return currentProfile; } } + + private static class CapturingEventBus extends EventBus { + + private List publishedEvents = new ArrayList<>(); + + @Override + public void post(Object event) { + publishedEvents.add(event); + super.post(event); + } + } } diff --git a/view/src/main/java/se/su/dsv/scipro/finalseminar/AdminFinalSeminarSettingsPage.html b/view/src/main/java/se/su/dsv/scipro/finalseminar/AdminFinalSeminarSettingsPage.html index a17eec8b77..7b95106394 100755 --- a/view/src/main/java/se/su/dsv/scipro/finalseminar/AdminFinalSeminarSettingsPage.html +++ b/view/src/main/java/se/su/dsv/scipro/finalseminar/AdminFinalSeminarSettingsPage.html @@ -42,6 +42,13 @@ +
+ +
+ +
+
+
diff --git a/view/src/main/java/se/su/dsv/scipro/finalseminar/AdminFinalSeminarSettingsPage.java b/view/src/main/java/se/su/dsv/scipro/finalseminar/AdminFinalSeminarSettingsPage.java index 59dad683ab..f04ffe4136 100755 --- a/view/src/main/java/se/su/dsv/scipro/finalseminar/AdminFinalSeminarSettingsPage.java +++ b/view/src/main/java/se/su/dsv/scipro/finalseminar/AdminFinalSeminarSettingsPage.java @@ -131,6 +131,17 @@ public class AdminFinalSeminarSettingsPage extends AbstractAdminSystemPage { Integer.class ) ); + add( + new RequiredTextField<>( + "work_days_to_fix_requested_improvements_to_opposition_report", + LambdaModel.of( + model, + FinalSeminarSettings::getWorkDaysToFixRequestedImprovementsToOppositionReport, + FinalSeminarSettings::setWorkDaysToFixRequestedImprovementsToOppositionReport + ), + Integer.class + ) + ); add( new CheckBox( SEMINAR_PDF, diff --git a/view/src/main/java/se/su/dsv/scipro/finalseminar/OppositionReportPage.html b/view/src/main/java/se/su/dsv/scipro/finalseminar/OppositionReportPage.html index f4df0cf012..62050c9116 100644 --- a/view/src/main/java/se/su/dsv/scipro/finalseminar/OppositionReportPage.html +++ b/view/src/main/java/se/su/dsv/scipro/finalseminar/OppositionReportPage.html @@ -9,9 +9,9 @@
- Använd bedömningskriterierna i denna rapport och skriv dina synpunkter som opponent i fritextfälten - under varje kriterium. Du gör dock ingen poängbedömning men - är fri att skriva så mycket som du önskar på varje bedömningskriterium. + Use the assessment criteria in this report and write your views as an opponent in the text + fields under each criterion. However, you do not make a point assessment but are free to + write as much as you wish on each criterion.
@@ -20,11 +20,22 @@ Final seminar file:
+ +
+

+ The supervisor has requested improvements to your opposition report. + You have until + to make the requested changes. See below for the comments from the supervisor. +

+

+
+
+
Thesis summary

- Ge en kort sammanfattning av det utvärderade arbetet. + Give a short summary of the evaluated work.