Allow supervisors to request improvements from final seminar opponents #78

Open
ansv7779 wants to merge 30 commits from opponent-completion into develop
52 changed files with 1148 additions and 251 deletions

View File

@ -31,6 +31,7 @@ import se.su.dsv.scipro.finalseminar.AuthorRepository;
import se.su.dsv.scipro.finalseminar.FinalSeminarActiveParticipationRepository; import se.su.dsv.scipro.finalseminar.FinalSeminarActiveParticipationRepository;
import se.su.dsv.scipro.finalseminar.FinalSeminarActiveParticipationServiceImpl; import se.su.dsv.scipro.finalseminar.FinalSeminarActiveParticipationServiceImpl;
import se.su.dsv.scipro.finalseminar.FinalSeminarCreationSubscribers; 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.FinalSeminarOppositionRepo;
import se.su.dsv.scipro.finalseminar.FinalSeminarOppositionServiceImpl; import se.su.dsv.scipro.finalseminar.FinalSeminarOppositionServiceImpl;
import se.su.dsv.scipro.finalseminar.FinalSeminarRepository; import se.su.dsv.scipro.finalseminar.FinalSeminarRepository;
@ -153,8 +154,8 @@ import se.su.dsv.scipro.report.GradingReportServiceImpl;
import se.su.dsv.scipro.report.GradingReportTemplateRepo; import se.su.dsv.scipro.report.GradingReportTemplateRepo;
import se.su.dsv.scipro.report.GradingReportTemplateRepoImpl; import se.su.dsv.scipro.report.GradingReportTemplateRepoImpl;
import se.su.dsv.scipro.report.OppositionReportRepo; 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.OppositionReportServiceImpl;
import se.su.dsv.scipro.report.ReportServiceImpl;
import se.su.dsv.scipro.report.SupervisorGradingReportRepository; import se.su.dsv.scipro.report.SupervisorGradingReportRepository;
import se.su.dsv.scipro.reviewing.DecisionRepository; import se.su.dsv.scipro.reviewing.DecisionRepository;
import se.su.dsv.scipro.reviewing.FinalSeminarApprovalService; import se.su.dsv.scipro.reviewing.FinalSeminarApprovalService;
@ -430,8 +431,26 @@ public class CoreConfig {
} }
@Bean @Bean
public FinalSeminarOppositionServiceImpl finalSeminarOppositionService(Provider<EntityManager> em) { public FinalSeminarOppositionServiceImpl finalSeminarOppositionService(
return new FinalSeminarOppositionServiceImpl(em); Provider<EntityManager> 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 @Bean
@ -669,13 +688,15 @@ public class CoreConfig {
OppositionReportRepo oppositionReportRepository, OppositionReportRepo oppositionReportRepository,
GradingReportTemplateRepo gradingReportTemplateRepository, GradingReportTemplateRepo gradingReportTemplateRepository,
FileService fileService, FileService fileService,
FinalSeminarOppositionRepo finalSeminarOppositionRepository FinalSeminarOppositionRepo finalSeminarOppositionRepository,
EventBus eventBus
) { ) {
return new OppositionReportServiceImpl( return new OppositionReportServiceImpl(
oppositionReportRepository, oppositionReportRepository,
gradingReportTemplateRepository, gradingReportTemplateRepository,
fileService, fileService,
finalSeminarOppositionRepository finalSeminarOppositionRepository,
eventBus
); );
} }
@ -855,11 +876,6 @@ public class CoreConfig {
); );
} }
@Bean
public ReportServiceImpl reportService(Provider<EntityManager> em, FileService fileService) {
return new ReportServiceImpl(em, fileService);
}
@Bean @Bean
public ResearchAreaServiceImpl researchAreaService(Provider<EntityManager> em) { public ResearchAreaServiceImpl researchAreaService(Provider<EntityManager> em) {
return new ResearchAreaServiceImpl(em); return new ResearchAreaServiceImpl(em);

View File

@ -4,16 +4,29 @@ import jakarta.inject.Inject;
import jakarta.inject.Provider; import jakarta.inject.Provider;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalTime; import java.time.LocalTime;
import java.time.Month; import java.time.Month;
import java.time.ZonedDateTime;
import java.util.*; import java.util.*;
import java.util.function.Function;
import se.su.dsv.scipro.checklist.ChecklistCategory; import se.su.dsv.scipro.checklist.ChecklistCategory;
import se.su.dsv.scipro.data.dataobjects.Member;
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.ApplicationPeriod;
import se.su.dsv.scipro.match.Keyword; import se.su.dsv.scipro.match.Keyword;
import se.su.dsv.scipro.milestones.dataobjects.MilestoneActivityTemplate; import se.su.dsv.scipro.milestones.dataobjects.MilestoneActivityTemplate;
import se.su.dsv.scipro.milestones.dataobjects.MilestonePhaseTemplate; import se.su.dsv.scipro.milestones.dataobjects.MilestonePhaseTemplate;
import se.su.dsv.scipro.milestones.service.MilestoneActivityTemplateService; import se.su.dsv.scipro.milestones.service.MilestoneActivityTemplateService;
import se.su.dsv.scipro.notifications.dataobject.Notification;
import se.su.dsv.scipro.notifications.dataobject.SeminarEvent;
import se.su.dsv.scipro.notifications.settings.service.ReceiverConfigurationService;
import se.su.dsv.scipro.profiles.CurrentProfile; import se.su.dsv.scipro.profiles.CurrentProfile;
import se.su.dsv.scipro.profiles.Profiles; import se.su.dsv.scipro.profiles.Profiles;
import se.su.dsv.scipro.project.Project; import se.su.dsv.scipro.project.Project;
@ -39,12 +52,18 @@ public class DataInitializer implements Lifecycle {
@Inject @Inject
private MilestoneActivityTemplateService milestoneActivityTemplateService; private MilestoneActivityTemplateService milestoneActivityTemplateService;
@Inject
private FileService fileService;
@Inject @Inject
private CurrentProfile profile; private CurrentProfile profile;
@Inject @Inject
private Provider<EntityManager> em; private Provider<EntityManager> em;
@Inject
private ReceiverConfigurationService receiverConfigurationService;
private static final String MAIL = "@example.com"; private static final String MAIL = "@example.com";
private static final String ADMIN = "admin"; private static final String ADMIN = "admin";
@ -75,6 +94,8 @@ public class DataInitializer implements Lifecycle {
private ResearchArea researchArea2; private ResearchArea researchArea2;
private ProjectType masterClass; private ProjectType masterClass;
private ProjectType magisterClass; private ProjectType magisterClass;
private Project project1;
private Project project2;
@Transactional @Transactional
@Override @Override
@ -89,12 +110,45 @@ public class DataInitializer implements Lifecycle {
createMilestonesIfNotDone(); createMilestonesIfNotDone();
createUsers(); createUsers();
createProjects(); createProjects();
createPastFinalSeminar();
setUpNotifications();
} }
if (profile.getCurrentProfile() == Profiles.DEV && noAdminUser()) { if (profile.getCurrentProfile() == Profiles.DEV && noAdminUser()) {
createAdmin(); createAdmin();
} }
} }
private void setUpNotifications() {
receiverConfigurationService.setReceiving(
Notification.Type.FINAL_SEMINAR,
SeminarEvent.Event.OPPOSITION_REPORT_SUBMITTED,
Member.Type.SUPERVISOR,
true
);
}
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 @Override
public void stop() {} public void stop() {}
@ -145,11 +199,11 @@ public class DataInitializer implements Lifecycle {
} }
private void createProjects() { private void createProjects() {
createProject(PROJECT_1, eric_employee, sture_student, stina_student, eve_employee); project1 = createProject(PROJECT_1, eric_employee, sture_student, stina_student, eve_employee);
createProject(PROJECT_2, eve_employee, sid_student, simon_student, eric_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() Project project = Project.builder()
.title(title) .title(title)
.projectType(bachelorClass) .projectType(bachelorClass)
@ -159,7 +213,7 @@ public class DataInitializer implements Lifecycle {
project.addProjectParticipant(student2); project.addProjectParticipant(student2);
project.addProjectParticipant(student1); project.addProjectParticipant(student1);
project.addReviewer(reviewer); project.addReviewer(reviewer);
save(project); return save(project);
} }
private void createUsers() { private void createUsers() {
@ -1907,4 +1961,42 @@ public class DataInitializer implements Lifecycle {
em.get().persist(entity); em.get().persist(entity);
return 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> T handleData(Function<InputStream, T> handler) {
return handler.apply(new ByteArrayInputStream(content.getBytes()));
}
}
} }

View File

@ -1,5 +1,6 @@
package se.su.dsv.scipro.finalseminar; package se.su.dsv.scipro.finalseminar;
import java.util.Objects;
import se.su.dsv.scipro.project.Project; import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
@ -26,4 +27,17 @@ class AbstractOppositionEvent {
public FinalSeminarOpposition getOpposition() { public FinalSeminarOpposition getOpposition() {
return opposition; 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);
}
} }

View File

@ -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<ExpireUnfulfilledOppositionImprovementsWorker> worker) {
scheduler
.schedule("Fail opponents that have not submitted improvements")
.runBy(worker)
.every(1, TimeUnit.HOURS);
}
}
}

View File

@ -8,6 +8,7 @@ import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne; import jakarta.persistence.OneToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.Instant;
import java.util.Objects; import java.util.Objects;
import se.su.dsv.scipro.file.FileReference; import se.su.dsv.scipro.file.FileReference;
import se.su.dsv.scipro.project.Project; import se.su.dsv.scipro.project.Project;
@ -31,6 +32,14 @@ public class FinalSeminarOpposition extends FinalSeminarParticipation {
@Column(name = "feedback", length = FEEDBACK_LENGTH) @Column(name = "feedback", length = FEEDBACK_LENGTH)
private String feedback; 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 // JPA-mappings of foreign keys in this table (final_seminar_opposition) referencing
// other tables. // other tables.
@ -92,6 +101,22 @@ public class FinalSeminarOpposition extends FinalSeminarParticipation {
this.oppositionReport = oppositionReport; 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 // Methods Common To All Objects
// ---------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------

View File

@ -0,0 +1,7 @@
package se.su.dsv.scipro.finalseminar;
import java.util.List;
public interface FinalSeminarOppositionGrading {
OppositionCriteria oppositionCriteria(FinalSeminarOpposition opposition);
}

View File

@ -11,4 +11,6 @@ import se.su.dsv.scipro.system.User;
public interface FinalSeminarOppositionRepo public interface FinalSeminarOppositionRepo
extends JpaRepository<FinalSeminarOpposition, Long>, QueryDslPredicateExecutor<FinalSeminarOpposition> { extends JpaRepository<FinalSeminarOpposition, Long>, QueryDslPredicateExecutor<FinalSeminarOpposition> {
List<FinalSeminarOpposition> findByOpposingUserAndType(User user, ProjectType projectType); List<FinalSeminarOpposition> findByOpposingUserAndType(User user, ProjectType projectType);
Collection<FinalSeminarOpposition> findUnfulfilledOppositionImprovements();
} }

View File

@ -24,4 +24,13 @@ public class FinalSeminarOppositionRepoImpl
.where(QFinalSeminarOpposition.finalSeminarOpposition.project.projectType.eq(projectType)) .where(QFinalSeminarOpposition.finalSeminarOpposition.project.projectType.eq(projectType))
.fetch(); .fetch();
} }
@Override
public Collection<FinalSeminarOpposition> findUnfulfilledOppositionImprovements() {
return createQuery()
.innerJoin(QFinalSeminarOpposition.finalSeminarOpposition.oppositionReport)
.where(QFinalSeminarOpposition.finalSeminarOpposition.improvementsRequestedAt.isNotNull())
.where(QFinalSeminarOpposition.finalSeminarOpposition.oppositionReport.submitted.isFalse())
.fetch();
}
} }

View File

@ -1,8 +1,18 @@
package se.su.dsv.scipro.finalseminar; package se.su.dsv.scipro.finalseminar;
import java.time.Instant;
import se.su.dsv.scipro.system.GenericService; import se.su.dsv.scipro.system.GenericService;
public interface FinalSeminarOppositionService extends GenericService<FinalSeminarOpposition, Long> { public interface FinalSeminarOppositionService {
@Override OppositionCriteria getCriteriaForOpposition(FinalSeminarOpposition opposition);
void delete(Long aLong);
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);
} }

View File

@ -1,16 +1,177 @@
package se.su.dsv.scipro.finalseminar; package se.su.dsv.scipro.finalseminar;
import com.google.common.eventbus.EventBus;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.inject.Provider; import jakarta.inject.Provider;
import jakarta.persistence.EntityManager; 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; import se.su.dsv.scipro.system.AbstractServiceImpl;
public class FinalSeminarOppositionServiceImpl public class FinalSeminarOppositionServiceImpl
extends AbstractServiceImpl<FinalSeminarOpposition, Long> extends AbstractServiceImpl<FinalSeminarOpposition, Long>
implements FinalSeminarOppositionService { 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 @Inject
public FinalSeminarOppositionServiceImpl(Provider<EntityManager> em) { public FinalSeminarOppositionServiceImpl(
Provider<EntityManager> em,
FinalSeminarOppositionGrading finalSeminarOppositionGrading,
EventBus eventBus,
FinalSeminarOppositionRepo finalSeminarOppositionRepository,
Clock clock,
FinalSeminarSettingsService finalSeminarSettingsService,
DaysService daysService,
OppositionReportService oppositionReportService
) {
super(em, FinalSeminarOpposition.class, QFinalSeminarOpposition.finalSeminarOpposition); 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<Opposition.ImprovementsNeeded> improvements = getImprovementsNeeded(finalSeminarOpposition);
return new Opposition(finalSeminarOpposition.getUser(), report, improvements);
}
private Optional<Opposition.ImprovementsNeeded> getImprovementsNeeded(
FinalSeminarOpposition finalSeminarOpposition
) {
if (finalSeminarOpposition.getSupervisorCommentForImprovements() != null) {
return Optional.of(
new Opposition.ImprovementsNeeded(
finalSeminarOpposition.getSupervisorCommentForImprovements(),
finalSeminarOpposition.getImprovementsRequestedAt()
)
);
} else {
return Optional.empty();
}
}
void expireUnfulfilledOppositionImprovements() {
Collection<FinalSeminarOpposition> 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);
}
}
} }
} }

View File

@ -2,7 +2,10 @@ package se.su.dsv.scipro.finalseminar;
import com.google.common.eventbus.EventBus; import com.google.common.eventbus.EventBus;
import com.querydsl.core.BooleanBuilder; import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.SubQueryExpressionImpl;
import com.querydsl.core.types.dsl.BooleanExpression; 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.Inject;
import jakarta.inject.Provider; import jakarta.inject.Provider;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
@ -542,19 +545,22 @@ public class FinalSeminarServiceImpl extends AbstractServiceImpl<FinalSeminar, L
private BooleanExpression unfinishedSeminars(Date after, Date before) { private BooleanExpression unfinishedSeminars(Date after, Date before) {
QFinalSeminar seminar = QFinalSeminar.finalSeminar; QFinalSeminar seminar = QFinalSeminar.finalSeminar;
if (after == null && before == null) { QFinalSeminarOpposition opposition = QFinalSeminarOpposition.finalSeminarOpposition;
return seminar.oppositions BooleanExpression ungradedParticipant = Expressions.anyOf(
seminar.oppositions
.any() .any()
.grade.isNull() .id.in(
.or(seminar.activeParticipations.any().grade.isNull().or(seminar.respondents.any().grade.isNull())); JPAExpressions.select(opposition.id)
.from(opposition)
.where(opposition.grade.isNull().and(opposition.improvementsRequestedAt.isNull()))
),
seminar.activeParticipations.any().grade.isNull(),
seminar.respondents.any().grade.isNull()
);
if (after == null && before == null) {
return ungradedParticipant;
} else { } else {
return seminar.startDate return seminar.startDate.between(after, before).and(ungradedParticipant);
.between(after, before)
.andAnyOf(
seminar.oppositions.any().grade.isNull(),
seminar.activeParticipations.any().grade.isNull(),
seminar.respondents.any().grade.isNull()
);
} }
} }

View File

@ -34,6 +34,9 @@ public class FinalSeminarSettings extends DomainObject {
@Column(name = "days_ahead_to_upload_thesis", nullable = false) @Column(name = "days_ahead_to_upload_thesis", nullable = false)
private int daysAheadToUploadThesis = DEFAULT_DAYS_AHEAD_TO_UPLOAD_THESIS; private int daysAheadToUploadThesis = DEFAULT_DAYS_AHEAD_TO_UPLOAD_THESIS;
@Column(name = "work_days_to_fix_requested_improvements_to_opposition_report", nullable = false)
private int workDaysToFixRequestedImprovementsToOppositionReport = 10;
@Column(name = "thesis_must_be_pdf", nullable = false) @Column(name = "thesis_must_be_pdf", nullable = false)
private boolean thesisMustBePDF = false; private boolean thesisMustBePDF = false;
@ -113,6 +116,17 @@ public class FinalSeminarSettings extends DomainObject {
this.oppositionPriorityDays = oppositionPriorityDays; this.oppositionPriorityDays = oppositionPriorityDays;
} }
public int getWorkDaysToFixRequestedImprovementsToOppositionReport() {
return workDaysToFixRequestedImprovementsToOppositionReport;
}
public void setWorkDaysToFixRequestedImprovementsToOppositionReport(
int workDaysToFixRequestedImprovementsToOppositionReport
) {
this.workDaysToFixRequestedImprovementsToOppositionReport =
workDaysToFixRequestedImprovementsToOppositionReport;
}
@Override @Override
public String toString() { public String toString() {
return ( return (

View File

@ -0,0 +1,10 @@
package se.su.dsv.scipro.finalseminar;
import java.time.Instant;
import java.util.Optional;
import se.su.dsv.scipro.report.OppositionReport;
import se.su.dsv.scipro.system.User;
public record Opposition(User user, OppositionReport report, Optional<ImprovementsNeeded> improvementsNeeded) {
record ImprovementsNeeded(String comment, Instant deadline) {}
}

View File

@ -0,0 +1,7 @@
package se.su.dsv.scipro.finalseminar;
import java.util.List;
public record OppositionCriteria(int pointsToPass, List<Point> pointsAvailable) {
public record Point(int value, String requirement) {}
}

View File

@ -0,0 +1,9 @@
package se.su.dsv.scipro.finalseminar;
import java.time.Instant;
public record OppositionReportImprovementsRequestedEvent(
FinalSeminarOpposition opposition,
String supervisorComment,
Instant deadline
) {}

View File

@ -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<Integer> acceptableValues;
public PointNotValidException(int givenValue, List<Integer> acceptableValues) {
this.givenValue = givenValue;
this.acceptableValues = acceptableValues;
}
public int givenValue() {
return givenValue;
}
public List<Integer> acceptableValues() {
return acceptableValues;
}
@Override
public String toString() {
return "PointNotValidException{" + "givenValue=" + givenValue + ", acceptableValues=" + acceptableValues + '}';
}
}

View File

@ -1,6 +1,9 @@
package se.su.dsv.scipro.misc; package se.su.dsv.scipro.misc;
import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date; import java.util.Date;
public interface DaysService { public interface DaysService {
@ -9,4 +12,11 @@ public interface DaysService {
int workDaysBetween(Date startDate, Date endDate); int workDaysBetween(Date startDate, Date endDate);
LocalDate workDaysAhead(LocalDate date, int days); LocalDate workDaysAhead(LocalDate date, int days);
LocalDate workDaysAfter(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();
}
} }

View File

@ -12,6 +12,7 @@ import se.su.dsv.scipro.finalseminar.FinalSeminarDeletedEvent;
import se.su.dsv.scipro.finalseminar.FinalSeminarThesisDeletedEvent; import se.su.dsv.scipro.finalseminar.FinalSeminarThesisDeletedEvent;
import se.su.dsv.scipro.finalseminar.FinalSeminarThesisUploadedEvent; import se.su.dsv.scipro.finalseminar.FinalSeminarThesisUploadedEvent;
import se.su.dsv.scipro.finalseminar.OppositionFailedEvent; 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.finalseminar.ParticipationFailedEvent;
import se.su.dsv.scipro.notifications.dataobject.NotificationSource; import se.su.dsv.scipro.notifications.dataobject.NotificationSource;
import se.su.dsv.scipro.notifications.dataobject.PeerEvent; import se.su.dsv.scipro.notifications.dataobject.PeerEvent;
@ -23,6 +24,7 @@ import se.su.dsv.scipro.project.ProjectActivatedEvent;
import se.su.dsv.scipro.project.ProjectCompletedEvent; import se.su.dsv.scipro.project.ProjectCompletedEvent;
import se.su.dsv.scipro.project.ProjectDeactivatedEvent; import se.su.dsv.scipro.project.ProjectDeactivatedEvent;
import se.su.dsv.scipro.project.ReviewerAssignedEvent; import se.su.dsv.scipro.project.ReviewerAssignedEvent;
import se.su.dsv.scipro.report.OppositionReportSubmittedEvent;
@Singleton @Singleton
public class Notifications { public class Notifications {
@ -168,6 +170,31 @@ public class Notifications {
); );
} }
@Subscribe
public void oppositionReportImprovementsRequested(OppositionReportImprovementsRequestedEvent event) {
Member recipient = new Member(event.opposition().getUser(), Member.Type.OPPONENT);
Set<Member> recipients = Set.of(recipient);
NotificationSource source = new NotificationSource();
source.setMessage(event.supervisorComment());
notificationController.notifyCustomSeminar(
event.opposition().getFinalSeminar(),
SeminarEvent.Event.OPPOSITION_REPORT_IMPROVEMENTS_REQUESTED,
source,
recipients
);
}
@Subscribe
public void oppositionReportSubmitted(OppositionReportSubmittedEvent event) {
NotificationSource source = new NotificationSource();
source.setMessage(event.report().getAuthorName());
notificationController.notifySeminar(
event.finalSeminar(),
SeminarEvent.Event.OPPOSITION_REPORT_SUBMITTED,
source
);
}
@Subscribe @Subscribe
public void reviewersChanged(ReviewerAssignedEvent event) { public void reviewersChanged(ReviewerAssignedEvent event) {
notificationController.notifyProject( notificationController.notifyProject(

View File

@ -26,6 +26,8 @@ public class SeminarEvent extends NotificationEvent {
THESIS_DELETED, THESIS_DELETED,
THESIS_UPLOAD_REMIND, THESIS_UPLOAD_REMIND,
CANCELLED, CANCELLED,
OPPOSITION_REPORT_SUBMITTED,
OPPOSITION_REPORT_IMPROVEMENTS_REQUESTED,
} }
@Basic @Basic

View File

@ -140,6 +140,12 @@ FINAL_SEMINAR.THESIS_UPLOAD_REMIND.body = No final seminar thesis has been uploa
If no final thesis has been uploaded by {0}, the final seminar will be automatically cancelled. If no final thesis has been uploaded by {0}, the final seminar will be automatically cancelled.
FINAL_SEMINAR.CANCELLED.title = Final seminar for project {1} was cancelled FINAL_SEMINAR.CANCELLED.title = Final seminar for project {1} was cancelled
FINAL_SEMINAR.CANCELLED.body = The final seminar for project {0} was cancelled, supervisor must select a new date for the final seminar. FINAL_SEMINAR.CANCELLED.body = The final seminar for project {0} was cancelled, supervisor must select a new date for the final seminar.
FINAL_SEMINAR.OPPOSITION_REPORT_SUBMITTED.title=Opposition report submitted by {1} for the seminar on project {0}
FINAL_SEMINAR.OPPOSITION_REPORT_SUBMITTED.body=The opposition report from {0} has been submitted.
FINAL_SEMINAR.OPPOSITION_REPORT_IMPROVEMENTS_REQUESTED.title = Opposition report improvements requested
FINAL_SEMINAR.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}
FINAL_SEMINAR.compilationSuffix = , project: {0} FINAL_SEMINAR.compilationSuffix = , project: {0}
PEER.REVIEW_COMPLETED.title = Peer review completed PEER.REVIEW_COMPLETED.title = Peer review completed

View File

@ -2,7 +2,6 @@ package se.su.dsv.scipro.report;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition;
import se.su.dsv.scipro.grading.GradingBasis; import se.su.dsv.scipro.grading.GradingBasis;
import se.su.dsv.scipro.project.Project; import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
@ -19,8 +18,6 @@ public interface GradingReportService {
SupervisorGradingReport supervisorGradingReport SupervisorGradingReport supervisorGradingReport
); );
boolean updateOppositionCriteria(SupervisorGradingReport report, FinalSeminarOpposition opposition);
GradingBasis getGradingBasis(Project project); GradingBasis getGradingBasis(Project project);
GradingBasis updateGradingBasis(Project project, GradingBasis gradingBasis); GradingBasis updateGradingBasis(Project project, GradingBasis gradingBasis);

View File

@ -1,6 +1,7 @@
package se.su.dsv.scipro.report; package se.su.dsv.scipro.report;
import com.google.common.eventbus.EventBus; import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import java.time.Clock; import java.time.Clock;
@ -8,6 +9,9 @@ import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.*; import java.util.*;
import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition; 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.GradingBasis;
import se.su.dsv.scipro.grading.GradingReportTemplateService; import se.su.dsv.scipro.grading.GradingReportTemplateService;
import se.su.dsv.scipro.grading.GradingReportTemplateUpdate; 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.system.User;
import se.su.dsv.scipro.util.Either; 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 EventBus eventBus;
private final ThesisSubmissionHistoryService thesisSubmissionHistoryService; private final ThesisSubmissionHistoryService thesisSubmissionHistoryService;
@ -44,11 +49,11 @@ public class GradingReportServiceImpl implements GradingReportTemplateService, G
this.supervisorGradingReportRepository = supervisorGradingReportRepository; this.supervisorGradingReportRepository = supervisorGradingReportRepository;
this.gradingReportTemplateRepo = gradingReportTemplateRepo; this.gradingReportTemplateRepo = gradingReportTemplateRepo;
this.projectTypeService = projectTypeService; this.projectTypeService = projectTypeService;
eventBus.register(this);
} }
@Override private boolean updateOppositionCriteria(SupervisorGradingReport report, FinalSeminarOpposition opposition) {
@Transactional
public boolean updateOppositionCriteria(SupervisorGradingReport report, FinalSeminarOpposition opposition) {
for (GradingCriterion gradingCriterion : report.getIndividualCriteria()) { for (GradingCriterion gradingCriterion : report.getIndividualCriteria()) {
boolean isOppositionCriterion = gradingCriterion.getFlag() == GradingCriterion.Flag.OPPOSITION; boolean isOppositionCriterion = gradingCriterion.getFlag() == GradingCriterion.Flag.OPPOSITION;
boolean betterGrade = boolean betterGrade =
@ -289,4 +294,39 @@ public class GradingReportServiceImpl implements GradingReportTemplateService, G
return gradingReportTemplateRepo.createTemplate(projectType, update); 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<GradingCriterion> oppositionGradingCriteria = supervisorGradingReport
.getIndividualCriteria()
.stream()
.filter(individualCriterion -> individualCriterion.getFlag() == AbstractGradingCriterion.Flag.OPPOSITION)
.findAny();
if (oppositionGradingCriteria.isEmpty()) {
return new OppositionCriteria(0, List.of());
}
List<OppositionCriteria.Point> 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);
}
} }

View File

@ -1,5 +1,7 @@
package se.su.dsv.scipro.report; package se.su.dsv.scipro.report;
import java.util.Optional;
import se.su.dsv.scipro.file.FileUpload;
import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition; import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition;
public interface OppositionReportService { public interface OppositionReportService {
@ -7,4 +9,10 @@ public interface OppositionReportService {
void save(OppositionReport oppositionReport); void save(OppositionReport oppositionReport);
void deleteOppositionReport(FinalSeminarOpposition finalSeminarOpposition); void deleteOppositionReport(FinalSeminarOpposition finalSeminarOpposition);
void deleteOpponentReport(FinalSeminarOpposition modelObject); void deleteOpponentReport(FinalSeminarOpposition modelObject);
AttachmentReport submit(OppositionReport report);
void save(OppositionReport report, Optional<FileUpload> fileUpload);
void deleteAttachment(OppositionReport report);
} }

View File

@ -1,10 +1,13 @@
package se.su.dsv.scipro.report; package se.su.dsv.scipro.report;
import com.google.common.eventbus.EventBus;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.inject.Named; import jakarta.inject.Named;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import java.util.Optional;
import se.su.dsv.scipro.file.FileReference; import se.su.dsv.scipro.file.FileReference;
import se.su.dsv.scipro.file.FileService; import se.su.dsv.scipro.file.FileService;
import se.su.dsv.scipro.file.FileUpload;
import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition; import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition;
import se.su.dsv.scipro.finalseminar.FinalSeminarOppositionRepo; import se.su.dsv.scipro.finalseminar.FinalSeminarOppositionRepo;
@ -15,18 +18,21 @@ public class OppositionReportServiceImpl implements OppositionReportService {
private GradingReportTemplateRepo gradingReportTemplateRepo; private GradingReportTemplateRepo gradingReportTemplateRepo;
private FileService fileService; private FileService fileService;
private FinalSeminarOppositionRepo finalSeminarOppositionRepo; private FinalSeminarOppositionRepo finalSeminarOppositionRepo;
private final EventBus eventBus;
@Inject @Inject
public OppositionReportServiceImpl( public OppositionReportServiceImpl(
OppositionReportRepo oppositionReportRepo, OppositionReportRepo oppositionReportRepo,
GradingReportTemplateRepo gradingReportTemplateRepo, GradingReportTemplateRepo gradingReportTemplateRepo,
FileService fileService, FileService fileService,
FinalSeminarOppositionRepo finalSeminarOppositionRepo FinalSeminarOppositionRepo finalSeminarOppositionRepo,
EventBus eventBus
) { ) {
this.oppositionReportRepo = oppositionReportRepo; this.oppositionReportRepo = oppositionReportRepo;
this.gradingReportTemplateRepo = gradingReportTemplateRepo; this.gradingReportTemplateRepo = gradingReportTemplateRepo;
this.fileService = fileService; this.fileService = fileService;
this.finalSeminarOppositionRepo = finalSeminarOppositionRepo; this.finalSeminarOppositionRepo = finalSeminarOppositionRepo;
this.eventBus = eventBus;
} }
@Override @Override
@ -74,4 +80,36 @@ public class OppositionReportServiceImpl implements OppositionReportService {
finalSeminarOppositionRepo.save(finalSeminarOpposition); finalSeminarOppositionRepo.save(finalSeminarOpposition);
} }
} }
@Override
@Transactional
public OppositionReport submit(OppositionReport report) {
report.submit();
OppositionReport submitted = oppositionReportRepo.save(report);
eventBus.post(new OppositionReportSubmittedEvent(submitted));
return submitted;
}
@Override
@Transactional
public void save(OppositionReport report, Optional<FileUpload> fileUpload) {
storeReportFile(report, fileUpload);
save(report);
}
@Override
@Transactional
public void deleteAttachment(OppositionReport report) {
FileReference attachment = report.getAttachment();
report.setAttachment(null);
fileService.delete(attachment);
oppositionReportRepo.save(report);
}
private void storeReportFile(OppositionReport report, Optional<FileUpload> fileUpload) {
if (fileUpload.isPresent()) {
final FileReference reference = fileService.storeFile(fileUpload.get());
report.setAttachment(reference);
}
}
} }

View File

@ -0,0 +1,9 @@
package se.su.dsv.scipro.report;
import se.su.dsv.scipro.finalseminar.FinalSeminar;
public record OppositionReportSubmittedEvent(OppositionReport report) {
public FinalSeminar finalSeminar() {
return report().getFinalSeminarOpposition().getFinalSeminar();
}
}

View File

@ -1,13 +0,0 @@
package se.su.dsv.scipro.report;
import java.util.Optional;
import se.su.dsv.scipro.file.FileUpload;
import se.su.dsv.scipro.system.GenericService;
public interface ReportService extends GenericService<Report, Long> {
AttachmentReport submit(AttachmentReport report);
void save(AttachmentReport report, Optional<FileUpload> fileUpload);
void deleteAttachment(AttachmentReport report);
}

View File

@ -1,52 +0,0 @@
package se.su.dsv.scipro.report;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import java.util.Optional;
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.system.AbstractServiceImpl;
public class ReportServiceImpl extends AbstractServiceImpl<Report, Long> implements ReportService {
private final FileService fileDescriptionService;
@Inject
public ReportServiceImpl(Provider<EntityManager> em, final FileService fileDescriptionService) {
super(em, Report.class, QReport.report);
this.fileDescriptionService = fileDescriptionService;
}
@Override
@Transactional
public AttachmentReport submit(AttachmentReport report) {
report.submit();
return save(report);
}
@Override
@Transactional
public void save(AttachmentReport report, Optional<FileUpload> fileUpload) {
storeReportFile(report, fileUpload);
save(report);
}
@Override
@Transactional
public void deleteAttachment(AttachmentReport report) {
FileReference attachment = report.getAttachment();
report.setAttachment(null);
fileDescriptionService.delete(attachment);
save(report);
}
private void storeReportFile(AttachmentReport report, Optional<FileUpload> fileUpload) {
if (fileUpload.isPresent()) {
final FileReference reference = fileDescriptionService.storeFile(fileUpload.get());
report.setAttachment(reference);
}
}
}

View File

@ -0,0 +1,3 @@
ALTER TABLE `final_seminar_opposition`
ADD COLUMN `improvements_requested_at` DATETIME NULL,
ADD COLUMN `supervisor_improvements_comment` TEXT NULL;

View File

@ -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;

View File

@ -1,14 +1,22 @@
package se.su.dsv.scipro.finalseminar; 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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.Month; import java.time.Month;
import java.util.Date; import java.util.Date;
import java.util.List;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import se.su.dsv.scipro.project.Project; 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.GradingReportTemplate;
import se.su.dsv.scipro.report.OppositionReport; import se.su.dsv.scipro.report.OppositionReport;
import se.su.dsv.scipro.system.DegreeType; import se.su.dsv.scipro.system.DegreeType;
@ -46,6 +54,76 @@ public class FinalSeminarOppositionServiceImplIntegrationTest extends Integratio
assertEquals(0, finalSeminarOppositionService.count()); 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) { private void createOppositionReport(FinalSeminarOpposition opposition) {
OppositionReport report = new OppositionReport(createGradingReportTemplate(), opposition); OppositionReport report = new OppositionReport(createGradingReportTemplate(), opposition);
opposition.setOppositionReport(report); opposition.setOppositionReport(report);
@ -93,7 +171,7 @@ public class FinalSeminarOppositionServiceImplIntegrationTest extends Integratio
FinalSeminarOpposition opposition = new FinalSeminarOpposition(); FinalSeminarOpposition opposition = new FinalSeminarOpposition();
opposition.setFinalSeminar(finalSeminar); opposition.setFinalSeminar(finalSeminar);
opposition.setUser(student); opposition.setUser(student);
opposition.setProject(createProject(createProjectType())); opposition.setProject(createProject(projectType));
return save(opposition); return save(opposition);
} }
} }

View File

@ -8,6 +8,7 @@ import static se.su.dsv.scipro.test.Matchers.isRight;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import java.time.Duration;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.Month; import java.time.Month;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
@ -30,6 +31,9 @@ public class FinalSeminarServiceImplIntegrationTest extends IntegrationTest {
@Inject @Inject
private FinalSeminarService finalSeminarService; private FinalSeminarService finalSeminarService;
@Inject
private FinalSeminarOppositionService finalSeminarOppositionService;
private ProjectType projectType; private ProjectType projectType;
private FinalSeminar futureFinalSeminar; private FinalSeminar futureFinalSeminar;
private User user; private User user;
@ -309,6 +313,43 @@ public class FinalSeminarServiceImplIntegrationTest extends IntegrationTest {
assertThat(finalSeminarService.canOppose(user, finalSeminar, otherProject), isRight(anything())); 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<FinalSeminar> unfinishedSeminars = finalSeminarService.findUnfinishedSeminars(
after,
before,
new PageRequest(0, 5)
);
assertThat(unfinishedSeminars, hasItem(seminar));
finalSeminarOppositionService.requestImprovements(finalSeminarOpposition, "improvements");
List<FinalSeminar> 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) { private FinalSeminar createFutureFinalSeminarSomeDaysAgo(final int daysAgo) {
FinalSeminar finalSeminar = initFinalSeminar(createProject(), 5); FinalSeminar finalSeminar = initFinalSeminar(createProject(), 5);
final Date dateCreated = Date.from(ZonedDateTime.now().minusDays(daysAgo).toInstant()); final Date dateCreated = Date.from(ZonedDateTime.now().minusDays(daysAgo).toInstant());
@ -340,10 +381,10 @@ public class FinalSeminarServiceImplIntegrationTest extends IntegrationTest {
save(seminar); save(seminar);
} }
private void addOpposition(FinalSeminar seminar, FinalSeminarGrade grade) { private FinalSeminarOpposition addOpposition(FinalSeminar seminar, FinalSeminarGrade grade) {
FinalSeminarOpposition opposition = createOpposition(seminar); FinalSeminarOpposition opposition = createOpposition(seminar);
opposition.setGrade(grade); opposition.setGrade(grade);
save(opposition); return save(opposition);
} }
private OppositionReport createOppositionReport(FinalSeminarOpposition opposition) { private OppositionReport createOppositionReport(FinalSeminarOpposition opposition) {

View File

@ -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.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import com.google.common.eventbus.EventBus;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.Month; import java.time.Month;
@ -13,6 +14,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import se.su.dsv.scipro.finalseminar.FinalSeminar; import se.su.dsv.scipro.finalseminar.FinalSeminar;
import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition; 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.project.Project;
import se.su.dsv.scipro.security.auth.roles.Roles; import se.su.dsv.scipro.security.auth.roles.Roles;
import se.su.dsv.scipro.system.DegreeType; import se.su.dsv.scipro.system.DegreeType;
@ -31,6 +33,9 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest {
@Inject @Inject
private GradingReportServiceImpl gradingReportService; private GradingReportServiceImpl gradingReportService;
@Inject
private EventBus eventBus;
private ProjectType projectType; private ProjectType projectType;
private GradingReportTemplate gradingReportTemplate; private GradingReportTemplate gradingReportTemplate;
private Project project; private Project project;
@ -45,7 +50,6 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest {
project = createProject(projectType, 30); project = createProject(projectType, 30);
gradingReportTemplate = createProjectGradingCriterion(gradingReportTemplate, 2); gradingReportTemplate = createProjectGradingCriterion(gradingReportTemplate, 2);
gradingReportTemplate = createIndividualGradingCriterion(gradingReportTemplate, 2); gradingReportTemplate = createIndividualGradingCriterion(gradingReportTemplate, 2);
gradingReport = createGradingReport(project, student);
} }
@Test @Test
@ -68,6 +72,7 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest {
@Test @Test
public void submit_supervisor_grading_report_flags_report_as_submitted() { public void submit_supervisor_grading_report_flags_report_as_submitted() {
gradingReport = createGradingReport(project, student);
assessAllCriteria(gradingReport); assessAllCriteria(gradingReport);
Either<List<SubmissionError>, SupervisorGradingReport> result = gradingReportService.submitReport( Either<List<SubmissionError>, SupervisorGradingReport> result = gradingReportService.submitReport(
gradingReport gradingReport
@ -77,6 +82,7 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest {
@Test @Test
public void submitting_supervisor_report_throws_exception_if_report_is_not_finished() { public void submitting_supervisor_report_throws_exception_if_report_is_not_finished() {
gradingReport = createGradingReport(project, student);
Either<List<SubmissionError>, SupervisorGradingReport> result = gradingReportService.submitReport( Either<List<SubmissionError>, SupervisorGradingReport> result = gradingReportService.submitReport(
gradingReport gradingReport
); );
@ -86,38 +92,35 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest {
@Test @Test
public void update_opposition_criterion() { public void update_opposition_criterion() {
addOppositionCriterion(); addOppositionCriterion();
boolean updated = updateOppositionCriterion(); updateOppositionCriterion();
GradingCriterion oppositionCriterion = findOppositionCriterion(); GradingCriterion oppositionCriterion = findOppositionCriterion();
assert oppositionCriterion != null; assert oppositionCriterion != null;
assertEquals(FEEDBACK_ON_OPPOSITION, oppositionCriterion.getFeedback()); assertEquals(FEEDBACK_ON_OPPOSITION, oppositionCriterion.getFeedback());
assertEquals((Integer) OPPOSITION_CRITERION_POINTS, oppositionCriterion.getPoints()); assertEquals((Integer) OPPOSITION_CRITERION_POINTS, oppositionCriterion.getPoints());
assertTrue(updated);
} }
@Test @Test
public void update_opposition_if_title_matches_english_title() { public void update_opposition_if_title_matches_english_title() {
addOppositionCriterion(); addOppositionCriterion();
boolean updated = updateOppositionCriterion(); updateOppositionCriterion();
GradingCriterion oppositionCriterion = findEnglishOppositionCriterion("Ö1 Opposition report"); GradingCriterion oppositionCriterion = findEnglishOppositionCriterion("Ö1 Opposition report");
assert oppositionCriterion != null; assert oppositionCriterion != null;
assertEquals(FEEDBACK_ON_OPPOSITION, oppositionCriterion.getFeedback()); assertEquals(FEEDBACK_ON_OPPOSITION, oppositionCriterion.getFeedback());
assertEquals((Integer) OPPOSITION_CRITERION_POINTS, oppositionCriterion.getPoints()); assertEquals((Integer) OPPOSITION_CRITERION_POINTS, oppositionCriterion.getPoints());
assertTrue(updated);
} }
@Test @Test
public void updating_opposition_criterion_does_nothing_if_criterion_already_has_values() { public void updating_opposition_criterion_does_nothing_if_criterion_already_has_values() {
addOppositionCriterion(); addOppositionCriterion();
assessAllCriteria(gradingReport); assessAllCriteria(gradingReport);
boolean updated = updateOppositionCriterion(); updateOppositionCriterion();
GradingCriterion oppositionCriterion = findOppositionCriterion(); GradingCriterion oppositionCriterion = findOppositionCriterion();
assert oppositionCriterion != null; assert oppositionCriterion != null;
assertEquals(FEEDBACK, oppositionCriterion.getFeedback()); assertEquals(FEEDBACK, oppositionCriterion.getFeedback());
assertEquals((Integer) oppositionCriterion.getMaxPoints(), oppositionCriterion.getPoints()); assertEquals((Integer) oppositionCriterion.getMaxPoints(), oppositionCriterion.getPoints());
assertFalse(updated);
} }
@Test @Test
@ -151,9 +154,9 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest {
gradingReport = createGradingReport(project, student); gradingReport = createGradingReport(project, student);
} }
private boolean updateOppositionCriterion() { private void updateOppositionCriterion() {
FinalSeminarOpposition opposition = createFinalSeminarOpposition(); FinalSeminarOpposition opposition = createFinalSeminarOpposition();
return gradingReportService.updateOppositionCriteria(gradingReport, opposition); eventBus.post(new OppositionApprovedEvent(opposition));
} }
private GradingCriterion findOppositionCriterion() { private GradingCriterion findOppositionCriterion() {
@ -176,8 +179,8 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest {
private FinalSeminarOpposition createFinalSeminarOpposition() { private FinalSeminarOpposition createFinalSeminarOpposition() {
FinalSeminarOpposition finalSeminarOpposition = new FinalSeminarOpposition(); FinalSeminarOpposition finalSeminarOpposition = new FinalSeminarOpposition();
finalSeminarOpposition.setProject(createProject(projectType, 30)); finalSeminarOpposition.setProject(project);
finalSeminarOpposition.setUser(createStudent()); finalSeminarOpposition.setUser(student);
finalSeminarOpposition.setFinalSeminar(createFinalSeminar()); finalSeminarOpposition.setFinalSeminar(createFinalSeminar());
finalSeminarOpposition.setFeedback(FEEDBACK_ON_OPPOSITION); finalSeminarOpposition.setFeedback(FEEDBACK_ON_OPPOSITION);

View File

@ -1,11 +1,14 @@
package se.su.dsv.scipro.test; package se.su.dsv.scipro.test;
import com.google.common.eventbus.EventBus;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction; import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence; import jakarta.persistence.Persistence;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.Clock; import java.time.Clock;
import java.util.ArrayList;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import org.flywaydb.core.Flyway; import org.flywaydb.core.Flyway;
@ -35,6 +38,8 @@ public abstract class SpringTest {
@Container @Container
static MariaDBContainer<?> mariaDBContainer = new MariaDBContainer<>("mariadb:10.11"); static MariaDBContainer<?> mariaDBContainer = new MariaDBContainer<>("mariadb:10.11");
private CapturingEventBus capturingEventBus;
@BeforeEach @BeforeEach
public final void prepareSpring() throws SQLException { public final void prepareSpring() throws SQLException {
MariaDbDataSource dataSource = new MariaDbDataSource(mariaDBContainer.getJdbcUrl()); MariaDbDataSource dataSource = new MariaDbDataSource(mariaDBContainer.getJdbcUrl());
@ -50,8 +55,11 @@ public abstract class SpringTest {
transaction.begin(); transaction.begin();
transaction.setRollbackOnly(); transaction.setRollbackOnly();
capturingEventBus = new CapturingEventBus();
AnnotationConfigApplicationContext annotationConfigApplicationContext = AnnotationConfigApplicationContext annotationConfigApplicationContext =
new AnnotationConfigApplicationContext(); new AnnotationConfigApplicationContext();
annotationConfigApplicationContext.registerBean("eventBus", EventBus.class, () -> this.capturingEventBus);
annotationConfigApplicationContext.register(TestContext.class); annotationConfigApplicationContext.register(TestContext.class);
annotationConfigApplicationContext.getBeanFactory().registerSingleton("entityManager", this.entityManager); annotationConfigApplicationContext.getBeanFactory().registerSingleton("entityManager", this.entityManager);
annotationConfigApplicationContext.refresh(); annotationConfigApplicationContext.refresh();
@ -75,6 +83,10 @@ public abstract class SpringTest {
} }
} }
protected List<Object> getPublishedEvents() {
return capturingEventBus.publishedEvents;
}
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@Import({ CoreConfig.class, RepositoryConfiguration.class }) @Import({ CoreConfig.class, RepositoryConfiguration.class })
public static class TestContext { public static class TestContext {
@ -106,4 +118,15 @@ public abstract class SpringTest {
return currentProfile; return currentProfile;
} }
} }
private static class CapturingEventBus extends EventBus {
private List<Object> publishedEvents = new ArrayList<>();
@Override
public void post(Object event) {
publishedEvents.add(event);
super.post(event);
}
}
} }

View File

@ -42,6 +42,13 @@
</div> </div>
</div> </div>
<div class="mb-3">
<label class="col-lg-4">How many work days opponents have to resubmit their report</label>
<div class="col-lg-1">
<input class="form-control" type="text" wicket:id="work_days_to_fix_requested_improvements_to_opposition_report" />
</div>
</div>
<div class="mb-3"> <div class="mb-3">
<div class="col-lg-offset-4 col-lg-4"> <div class="col-lg-offset-4 col-lg-4">
<div class="form-check"> <div class="form-check">

View File

@ -131,6 +131,17 @@ public class AdminFinalSeminarSettingsPage extends AbstractAdminSystemPage {
Integer.class Integer.class
) )
); );
add(
new RequiredTextField<>(
"work_days_to_fix_requested_improvements_to_opposition_report",
LambdaModel.of(
model,
FinalSeminarSettings::getWorkDaysToFixRequestedImprovementsToOppositionReport,
FinalSeminarSettings::setWorkDaysToFixRequestedImprovementsToOppositionReport
),
Integer.class
)
);
add( add(
new CheckBox( new CheckBox(
SEMINAR_PDF, SEMINAR_PDF,

View File

@ -6,29 +6,36 @@
<div class="col-lg-8"> <div class="col-lg-8">
<h4>Opposition report</h4> <h4>Opposition report</h4>
<div class="row mb-4"> <div class="help-box mb-3">
<div class="col-lg-8"> Use the assessment criteria in this report and write your views as an opponent in the text
<div class="help-box"> fields under each criterion. However, you do not make a point assessment but are free to
Använd bedömningskriterierna i denna rapport och skriv dina synpunkter som opponent i fritextfälten write as much as you wish on each criterion.
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.
</div>
</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<strong>Final seminar file:</strong> <span wicket:id="thesisFile"></span> <strong>Final seminar file:</strong> <span wicket:id="thesisFile"></span>
</div> </div>
<div wicket:id="fillOutReport"> <wicket:enclosure child="improvements_requested_comment">
<strong>Thesis summary</strong> <div class="alert alert-info">
<p> <p>
Ge en kort sammanfattning av det utvärderade arbetet. The supervisor has requested improvements to your opposition report.
You have until <span wicket:id="improvements_requested_deadline"></span>
to make the requested changes. See below for the comments from the supervisor.
</p> </p>
<label> <p class="mb-0" wicket:id="improvements_requested_comment"></p>
<textarea class="form-control mb-4" rows="8" wicket:id="thesisSummary"></textarea> </div>
</wicket:enclosure>
<div wicket:id="fillOutReport">
<label wicket:for="thesisSummary">
Thesis summary
</label> </label>
<p>
Give a short summary of the evaluated work.
</p>
<textarea class="form-control mb-4" rows="8" wicket:id="thesisSummary"></textarea>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,8 +1,11 @@
package se.su.dsv.scipro.finalseminar; package se.su.dsv.scipro.finalseminar;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import java.time.ZoneId;
import java.util.Optional;
import org.apache.wicket.RestartResponseException; import org.apache.wicket.RestartResponseException;
import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.TextArea; import org.apache.wicket.markup.html.form.TextArea;
import org.apache.wicket.model.IModel; import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LambdaModel; import org.apache.wicket.model.LambdaModel;
@ -24,7 +27,7 @@ public class OppositionReportPage extends AbstractProjectDetailsPage implements
public static final String FILL_OUT_REPORT = "fillOutReport"; public static final String FILL_OUT_REPORT = "fillOutReport";
@Inject @Inject
private FinalSeminarOppositionRepo finalSeminarOppositionRepo; private FinalSeminarOppositionService finalSeminarOppositionService;
@Inject @Inject
private OppositionReportService oppositionReportService; private OppositionReportService oppositionReportService;
@ -35,13 +38,15 @@ public class OppositionReportPage extends AbstractProjectDetailsPage implements
throw new RestartResponseException(ProjectDetailsPage.class, pp); throw new RestartResponseException(ProjectDetailsPage.class, pp);
} }
final FinalSeminarOpposition opposition = finalSeminarOppositionRepo.findOne(pp.get("oid").toLong()); final IModel<Opposition> opposition = LoadableDetachableModel.of(() ->
finalSeminarOppositionService.getOpposition(pp.get("oid").toLong())
);
if (opposition == null) { if (opposition.getObject() == null) {
throw new RestartResponseException(ProjectDetailsPage.class, pp); throw new RestartResponseException(ProjectDetailsPage.class, pp);
} }
final IModel<OppositionReport> report = getOppositionReport(opposition); final IModel<OppositionReport> report = opposition.map(Opposition::report);
add( add(
new ViewAttachmentPanel( new ViewAttachmentPanel(
@ -50,8 +55,35 @@ public class OppositionReportPage extends AbstractProjectDetailsPage implements
) )
); );
IModel<Opposition.ImprovementsNeeded> improvements = opposition
.map(Opposition::improvementsNeeded)
.map(OppositionReportPage::orNull);
add( add(
new FillOutReportPanel<>(FILL_OUT_REPORT, report) { new Label("improvements_requested_comment", improvements.map(Opposition.ImprovementsNeeded::comment)) {
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(!getDefaultModelObjectAsString().isBlank());
}
}
);
add(
new Label(
"improvements_requested_deadline",
improvements
.map(Opposition.ImprovementsNeeded::deadline)
.map(deadline -> deadline.atZone(ZoneId.systemDefault()))
) {
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(getDefaultModelObject() != null);
}
}
);
add(
new FillOutReportPanel(FILL_OUT_REPORT, report) {
{ {
TextArea<String> textArea = new TextArea<>( TextArea<String> textArea = new TextArea<>(
THESIS_SUMMARY, THESIS_SUMMARY,
@ -71,18 +103,13 @@ public class OppositionReportPage extends AbstractProjectDetailsPage implements
@Override @Override
protected void onConfigure() { protected void onConfigure() {
super.onConfigure(); super.onConfigure();
setEnabled(opposition.getUser().equals(SciProSession.get().getUser())); setEnabled(opposition.getObject().user().equals(SciProSession.get().getUser()));
} }
} }
); );
} }
private IModel<OppositionReport> getOppositionReport(final FinalSeminarOpposition opposition) { private static <A> A orNull(Optional<A> optional) {
return new LoadableDetachableModel<>() { return optional.orElse(null);
@Override
protected OppositionReport load() {
return oppositionReportService.findOrCreateReport(opposition);
}
};
} }
} }

View File

@ -11,7 +11,7 @@
<div wicket:id="container"> <div wicket:id="container">
<div wicket:id="opponents"> <div wicket:id="opponents">
<div class="row"> <div class="row">
<div class="col-lg-7 mb-3"> <div class="col-lg-7">
<span wicket:id="user"></span><a href="#" wicket:id="remove"><span class="fa fa-times"></span></a><br> <span wicket:id="user"></span><a href="#" wicket:id="remove"><span class="fa fa-times"></span></a><br>
<div wicket:id="report"></div> <div wicket:id="report"></div>
</div> </div>
@ -19,13 +19,31 @@
<div class="col-lg-5"> <div class="col-lg-5">
<form wicket:id="form"> <form wicket:id="form">
<div class="card mb-3 bg-info text-white"> <div class="card mb-3 text-bg-info">
<wicket:message key="criteria"/> <div class="card-body">
<p class="card-text">
<wicket:message key="criteria"/>
</p>
<p class="card-text" wicket:id="requirements">
<wicket:container wicket:id="requirement"/>
</p>
</div>
</div> </div>
<wicket:enclosure>
<div class="alert alert-info">
<p>
You've requested improvements to the opposition report with the below comment.
If they do not make the requested improvements in time, they will get an automatic failing grade.
The system will notify you when they've submitted a new report.
</p>
<span wicket:id="improvements_requested"></span>
</div>
</wicket:enclosure>
<div class="mb-3"> <div class="mb-3">
<label>Points:</label> <label>Points:</label>
<input type="text" class="form-control gradingPoints" wicket:id="points"/> <select class="form-select" wicket:id="points"></select>
</div> </div>
<label>Motivation:</label> <label>Motivation:</label>
@ -34,6 +52,20 @@
<button wicket:id="submit" type="submit" class="btn btn-sm btn-success"> <button wicket:id="submit" type="submit" class="btn btn-sm btn-success">
<wicket:message key="submit"/> <wicket:message key="submit"/>
</button> </button>
<a class="btn btn-outline-secondary btn-sm" wicket:id="request_improvements">
Request improvements
</a>
</form>
<form wicket:id="request_improvements">
<div class="mb-3">
<label class="form-label" wicket:for="feedback_to_opponent">
Provide feedback to the opponent on how to improve the opposition
</label>
<textarea class="form-control" wicket:id="feedback_to_opponent" rows="8"></textarea>
</div>
<button class="btn btn-sm btn-success">Request improvements</button>
<a class="btn btn-outline-secondary btn-sm" wicket:id="cancel">Cancel</a>
</form> </form>
<div wicket:id="gradeContainer"> <div wicket:id="gradeContainer">

View File

@ -2,18 +2,23 @@ package se.su.dsv.scipro.finalseminar;
import com.google.common.eventbus.EventBus; import com.google.common.eventbus.EventBus;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.ajax.markup.html.form.AjaxSubmitLink; import org.apache.wicket.ajax.markup.html.form.AjaxSubmitLink;
import org.apache.wicket.feedback.FencedFeedbackPanel; import org.apache.wicket.feedback.FencedFeedbackPanel;
import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.FormComponent; import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.markup.html.form.LambdaChoiceRenderer;
import org.apache.wicket.markup.html.form.TextArea; import org.apache.wicket.markup.html.form.TextArea;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.link.Link; import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.list.ListItem; import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.markup.html.list.ListView;
@ -22,14 +27,15 @@ import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel; import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LambdaModel; import org.apache.wicket.model.LambdaModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.ResourceModel; import org.apache.wicket.model.ResourceModel;
import org.apache.wicket.validation.validator.RangeValidator;
import org.apache.wicket.validation.validator.StringValidator; import org.apache.wicket.validation.validator.StringValidator;
import se.su.dsv.scipro.components.ListAdapterModel; import se.su.dsv.scipro.components.ListAdapterModel;
import se.su.dsv.scipro.components.StatelessModel;
import se.su.dsv.scipro.profile.UserLinkPanel; import se.su.dsv.scipro.profile.UserLinkPanel;
import se.su.dsv.scipro.report.GradingReportService; import se.su.dsv.scipro.report.GradingReportService;
import se.su.dsv.scipro.report.OppositionReportService; import se.su.dsv.scipro.report.OppositionReportService;
import se.su.dsv.scipro.report.SupervisorGradingReport;
import se.su.dsv.scipro.security.auth.roles.Roles; import se.su.dsv.scipro.security.auth.roles.Roles;
import se.su.dsv.scipro.session.SciProSession; import se.su.dsv.scipro.session.SciProSession;
import se.su.dsv.scipro.system.ProjectModule; import se.su.dsv.scipro.system.ProjectModule;
@ -43,8 +49,6 @@ public class SeminarOppositionPanel extends Panel {
public static final String REMOVE = "remove"; public static final String REMOVE = "remove";
public static final String FORM = "form"; public static final String FORM = "form";
public static final String POINTS = "points"; public static final String POINTS = "points";
public static final int MIN_POINTS = 0;
public static final int MAX_POINTS = 2;
public static final String GRADING_FEEDBACK = "gradingFeedback"; public static final String GRADING_FEEDBACK = "gradingFeedback";
public static final int FEEDBACK_MAX_LENGTH = 2000; public static final int FEEDBACK_MAX_LENGTH = 2000;
public static final String SUBMIT = "submit"; public static final String SUBMIT = "submit";
@ -75,6 +79,9 @@ public class SeminarOppositionPanel extends Panel {
private final WebMarkupContainer oppositionContainer; private final WebMarkupContainer oppositionContainer;
private final ListView<FinalSeminarOpposition> opponents; private final ListView<FinalSeminarOpposition> opponents;
private FinalSeminarOppositionForm gradeForm;
private RequestImprovementsForm requestImprovementsForm;
public SeminarOppositionPanel(String id, final IModel<FinalSeminar> seminar) { public SeminarOppositionPanel(String id, final IModel<FinalSeminar> seminar) {
super(id, seminar); super(id, seminar);
this.seminar = seminar; this.seminar = seminar;
@ -107,6 +114,12 @@ public class SeminarOppositionPanel extends Panel {
private ListView<FinalSeminarOpposition> getOpponentsList(final IModel<List<FinalSeminarOpposition>> oppositions) { private ListView<FinalSeminarOpposition> getOpponentsList(final IModel<List<FinalSeminarOpposition>> oppositions) {
return new ListView<>(OPPONENTS, oppositions) { return new ListView<>(OPPONENTS, oppositions) {
{
// Need to reuse child list items since they contain form components
// and if they're recreated all the state and error messages are lost
setReuseItems(true);
}
@Override @Override
protected void populateItem(final ListItem<FinalSeminarOpposition> item) { protected void populateItem(final ListItem<FinalSeminarOpposition> item) {
final FinalSeminarOpposition opposition = item.getModelObject(); final FinalSeminarOpposition opposition = item.getModelObject();
@ -121,7 +134,14 @@ public class SeminarOppositionPanel extends Panel {
item.add(getRemoveLink(item.getModel())); item.add(getRemoveLink(item.getModel()));
item.add(getFinalSeminarOppositionForm(item)); gradeForm = getFinalSeminarOppositionForm(item);
gradeForm.setOutputMarkupPlaceholderTag(true);
item.add(gradeForm);
requestImprovementsForm = new RequestImprovementsForm("request_improvements", item.getModel());
requestImprovementsForm.setVisible(false);
requestImprovementsForm.setOutputMarkupPlaceholderTag(true);
item.add(requestImprovementsForm);
if (gradingModuleIsOnForProjectType()) { if (gradingModuleIsOnForProjectType()) {
item.add(new SeminarOppositionReportPanel("report", item.getModel())); item.add(new SeminarOppositionReportPanel("report", item.getModel()));
@ -211,29 +231,47 @@ public class SeminarOppositionPanel extends Panel {
private class FinalSeminarOppositionForm extends Form<FinalSeminarOpposition> { private class FinalSeminarOppositionForm extends Form<FinalSeminarOpposition> {
private IModel<OppositionCriteria.Point> pointsModel = new StatelessModel<>();
private IModel<String> feedbackModel = new Model<>();
public FinalSeminarOppositionForm(String id, final IModel<FinalSeminarOpposition> finalSeminarOpposition) { public FinalSeminarOppositionForm(String id, final IModel<FinalSeminarOpposition> finalSeminarOpposition) {
super(id, finalSeminarOpposition); super(id, finalSeminarOpposition);
FormComponent<Integer> pointsField = new TextField<>( IModel<OppositionCriteria> criteriaModel = LoadableDetachableModel.of(() ->
finalSeminarOppositionService.getCriteriaForOpposition(finalSeminarOpposition.getObject())
);
add(
new ListView<>("requirements", criteriaModel.map(this::getPointsWithRequirements)) {
@Override
protected void populateItem(ListItem<OppositionCriteria.Point> item) {
item.add(new Label("requirement", item.getModel().map(OppositionCriteria.Point::requirement)));
}
}
);
IModel<String> improvementComment = finalSeminarOpposition.map(
FinalSeminarOpposition::getSupervisorCommentForImprovements
);
add(
new Label("improvements_requested", improvementComment) {
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(!getDefaultModelObjectAsString().isBlank());
}
}
);
FormComponent<OppositionCriteria.Point> pointsField = new DropDownChoice<>(
POINTS, POINTS,
LambdaModel.of( pointsModel,
finalSeminarOpposition, criteriaModel.map(OppositionCriteria::pointsAvailable),
FinalSeminarOpposition::getPoints, new LambdaChoiceRenderer<>(OppositionCriteria.Point::value)
FinalSeminarOpposition::setPoints );
) pointsField.setRequired(true);
)
.add(RangeValidator.range(MIN_POINTS, MAX_POINTS))
.setType(Integer.class)
.setRequired(true);
add(pointsField); add(pointsField);
TextArea<String> feedback = new TextArea<>( TextArea<String> feedback = new TextArea<>(GRADING_FEEDBACK, feedbackModel);
GRADING_FEEDBACK,
LambdaModel.of(
finalSeminarOpposition,
FinalSeminarOpposition::getFeedback,
FinalSeminarOpposition::setFeedback
)
);
feedback.add(StringValidator.maximumLength(FEEDBACK_MAX_LENGTH)); feedback.add(StringValidator.maximumLength(FEEDBACK_MAX_LENGTH));
feedback.setRequired(true); feedback.setRequired(true);
add(feedback); add(feedback);
@ -242,33 +280,19 @@ public class SeminarOppositionPanel extends Panel {
new AjaxSubmitLink(SUBMIT) { new AjaxSubmitLink(SUBMIT) {
@Override @Override
protected void onSubmit(AjaxRequestTarget target) { protected void onSubmit(AjaxRequestTarget target) {
if (getModelObject().getPoints().equals(0)) { try {
finalSeminarOpposition.getObject().setGrade(FinalSeminarGrade.NOT_APPROVED); finalSeminarOppositionService.gradeOpponent(
eventBus.post(new OppositionFailedEvent(finalSeminarOpposition.getObject())); finalSeminarOpposition.getObject(),
} else { pointsModel.getObject().value(),
finalSeminarOpposition.getObject().setGrade(FinalSeminarGrade.APPROVED); feedbackModel.getObject()
eventBus.post(new OppositionApprovedEvent(finalSeminarOpposition.getObject()));
}
finalSeminarOppositionService.save(finalSeminarOpposition.getObject());
boolean updated = true;
if (gradingModuleIsOnForProjectType()) {
SupervisorGradingReport report = gradingReportService.getSupervisorGradingReport(
finalSeminarOpposition.getObject().getProject(),
finalSeminarOpposition.getObject().getUser()
);
updated = gradingReportService.updateOppositionCriteria(
report,
finalSeminarOpposition.getObject()
); );
success(getString("feedback.opponent.updated", finalSeminarOpposition));
target.add(feedbackPanel);
target.add(oppositionContainer);
} catch (PointNotValidException e) {
error(getString("point.not.valid"));
target.add(feedbackPanel);
} }
success(
getString(
updated ? "feedback.opponent.updated" : "feedback.opponent.not.updated",
finalSeminarOpposition
)
);
target.add(feedbackPanel);
target.add(oppositionContainer);
} }
@Override @Override
@ -277,6 +301,31 @@ public class SeminarOppositionPanel extends Panel {
} }
} }
); );
add(
new AjaxLink<Void>("request_improvements") {
@Override
public void onClick(AjaxRequestTarget target) {
requestImprovementsForm.setVisible(true);
target.add(requestImprovementsForm);
gradeForm.setVisible(false);
target.add(gradeForm);
target.appendJavaScript(
"document.getElementById('" +
requestImprovementsForm.get("feedback_to_opponent").getMarkupId() +
"').focus();"
);
}
}
);
}
private List<OppositionCriteria.Point> getPointsWithRequirements(OppositionCriteria oppositionCriteria) {
return oppositionCriteria
.pointsAvailable()
.stream()
.filter(point -> !point.requirement().isBlank())
.toList();
} }
@Override @Override
@ -298,4 +347,47 @@ public class SeminarOppositionPanel extends Panel {
private boolean hasSubmittedOppositionReport(FinalSeminarOpposition opposition) { private boolean hasSubmittedOppositionReport(FinalSeminarOpposition opposition) {
return oppositionReportService.findOrCreateReport(opposition).isSubmitted(); return oppositionReportService.findOrCreateReport(opposition).isSubmitted();
} }
private class RequestImprovementsForm extends Form<FinalSeminarOpposition> {
private final Model<String> feedbackToOpponentModel = new Model<>();
public RequestImprovementsForm(String id, IModel<FinalSeminarOpposition> model) {
super(id, model);
TextArea<String> feedbackToOpponentField = new TextArea<>("feedback_to_opponent", feedbackToOpponentModel);
feedbackToOpponentField.setRequired(true);
add(feedbackToOpponentField);
add(
new AjaxLink<Void>("cancel") {
@Override
public void onClick(AjaxRequestTarget target) {
requestImprovementsForm.setVisible(false);
target.add(requestImprovementsForm);
gradeForm.setVisible(true);
target.add(gradeForm);
}
}
);
}
@Override
protected void onSubmit() {
Instant deadline = finalSeminarOppositionService.requestImprovements(
getModelObject(),
feedbackToOpponentModel.getObject()
);
record ImprovementFeedback(String fullName, ZonedDateTime deadline) {}
ZonedDateTime localDeadline = deadline.atZone(ZoneId.systemDefault());
success(
getString("feedback.opponent.requested.improvements", () ->
new ImprovementFeedback(getModelObject().getUser().getFullName(), localDeadline)
)
);
requestImprovementsForm.setVisible(false);
gradeForm.setVisible(true);
}
}
} }

View File

@ -8,16 +8,15 @@ gradingFeedback.Required = You need to write a motivation
points.Required = Points are required points.Required = Points are required
opponents.form.points.RangeValidator.range= Points assigned must be between ${minimum} and ${maximum} opponents.form.points.RangeValidator.range= Points assigned must be between ${minimum} and ${maximum}
feedback.opponent.updated= Opponent ${user.fullName} feedback updated. feedback.opponent.updated= Opponent ${user.fullName} feedback updated.
point.not.valid=You need to assign points from the available selection.
feedback.opponent.not.updated= Opponent ${user.fullName} feedback updated. Point and motivation could not be transferred to the students final grading report since the opponents supervisor already filled it in. feedback.opponent.not.updated= Opponent ${user.fullName} feedback updated. Point and motivation could not be transferred to the students final grading report since the opponents supervisor already filled it in.
criteria= As the supervisor on this final seminar you are also required to grade the opponents opposition report.\ criteria= As the supervisor on this final seminar you are also required to grade the opponents opposition report.
<\br><\br>Requirement for 1 point: <\br>That the opposition report provides a short summary of the evaluated thesis, that it deliberates about the scientific basis, originality, \
significance, and formulation of the problem and research question, as well as that it contains clear suggestions for improvements. \
<\br><\br>For 2 points the following is also required: \
<\br>That the opposition report thoroughly and in a well-balanced way describes from numerous aspects the strengths and weaknesses of the evaluated thesis and that it \
offers clear and well- motivated suggestions for improvements.
opposition.report= Opposition report: opposition.report= Opposition report:
removed= Opponent ${user.fullName} successfully removed removed= Opponent ${user.fullName} successfully removed
opposition.report.removed= Opposition report successfully removed opposition.report.removed= Opposition report successfully removed
are.you.sure= Are you sure you want to remove this opponent report? are.you.sure= Are you sure you want to remove this opponent report?
no.opponents= There are no opponents registered yet. no.opponents= There are no opponents registered yet.
noOppositionReportYet= No opposition report has been submitted yet. noOppositionReportYet= No opposition report has been submitted yet.
feedback.opponent.requested.improvements = You've requested improvements from ${fullName}. \
They have until ${deadline} to make the changes. If they fail to resubmit by that point they \
will automatically get a failing grade.

View File

@ -7,9 +7,13 @@
<div wicket:id="wmc"> <div wicket:id="wmc">
<wicket:enclosure child="newReport"> <wicket:enclosure child="newReport">
<wicket:container wicket:id="newReport"/> <wicket:container wicket:id="newReport"/>
<div class="alert alert-info mt-1 mb-1" wicket:id="improvements_requested">
The supervisor has requested improvements to your opposition report.
Click the link below to see detailed comments from the supervisor and to make the requested changes.
</div>
<span wicket:id="oppositionReportLabel"></span> <span wicket:id="noOppositionReportYet"></span> <span wicket:id="oppositionReportLabel"></span> <span wicket:id="noOppositionReportYet"></span>
<a href="#" wicket:id="oppositionReportLink">Fill out opposition report</a> <a href="#" wicket:id="oppositionReportLink">Fill out opposition report</a>
<span wicket:id="downloadPdfPanel"></span><br /> <div wicket:id="downloadPdfPanel"></div>
<wicket:enclosure child="downloadAttachment"> <wicket:enclosure child="downloadAttachment">
Report attachment: <span wicket:id="downloadAttachment"></span> Report attachment: <span wicket:id="downloadAttachment"></span>
</wicket:enclosure> </wicket:enclosure>

View File

@ -70,6 +70,19 @@ public class SeminarOppositionReportPanel extends GenericPanel<FinalSeminarOppos
wmc.add(getDeleteOpponentReportLink(model)); wmc.add(getDeleteOpponentReportLink(model));
wmc.add(getDeleteOppositionReportLink(model)); wmc.add(getDeleteOppositionReportLink(model));
wmc.add(
new WebMarkupContainer("improvements_requested") {
@Override
protected void onConfigure() {
super.onConfigure();
FinalSeminarOpposition opp = model.getObject();
boolean notGraded = opp.getGrade() == null;
boolean improvementsRequested = opp.getImprovementsRequestedAt() != null;
setVisible(isOpponentAndNotSubmitted(opp) && notGraded && improvementsRequested);
}
}
);
} }
private Component getNewReportContainer(ViewAttachmentPanel oldReport) { private Component getNewReportContainer(ViewAttachmentPanel oldReport) {

View File

@ -5,30 +5,26 @@
</head> </head>
<body> <body>
<wicket:border> <wicket:border>
<div class="row"> <div wicket:id="save"></div>
<div class="col-lg-8"> <form wicket:id="form">
<div wicket:id="save"></div> <div wicket:id="feedbackPanel"></div>
<form wicket:id="form"> <wicket:body/>
<div wicket:id="feedbackPanel"></div> <div wicket:id="criteria">
<wicket:body/> <strong><span wicket:id="title"></span></strong>
<div wicket:id="criteria">
<strong><span wicket:id="title"></span></strong>
<p><span wicket:id="description" class="gradingCriteria"></span></p> <p><span wicket:id="description" class="gradingCriteria"></span></p>
<textarea class="form-control mb-4" rows="8" cols="5" wicket:id="feedback"></textarea> <textarea class="form-control mb-4" rows="8" cols="5" wicket:id="feedback"></textarea>
</div>
<div>
<strong><wicket:message key="attachment" /></strong>
<span wicket:id="viewAttachment"></span>
<a wicket:id="deleteAttachment"><span class="fa fa-times"></span></a>
<input type="file" wicket:id="uploadAttachment" class="mb-3"/>
</div>
<button type="button" class="btn btn-success btn-sm mb-3" wicket:id="submit">
Submit
</button>
</form>
</div> </div>
</div> <div>
<strong><wicket:message key="attachment" /></strong>
<span wicket:id="viewAttachment"></span>
<a wicket:id="deleteAttachment"><span class="fa fa-times"></span></a>
<input type="file" wicket:id="uploadAttachment" class="mb-3"/>
</div>
<button type="button" class="btn btn-success btn-sm mb-3" wicket:id="submit">
Submit
</button>
</form>
</wicket:border> </wicket:border>
</body> </body>
</html> </html>

View File

@ -25,12 +25,12 @@ import se.su.dsv.scipro.files.WicketFileUpload;
import se.su.dsv.scipro.report.AttachmentReport; import se.su.dsv.scipro.report.AttachmentReport;
import se.su.dsv.scipro.report.Criterion; import se.su.dsv.scipro.report.Criterion;
import se.su.dsv.scipro.report.OppositionReport; import se.su.dsv.scipro.report.OppositionReport;
import se.su.dsv.scipro.report.ReportService; import se.su.dsv.scipro.report.OppositionReportService;
import se.su.dsv.scipro.repository.panels.ViewAttachmentPanel; import se.su.dsv.scipro.repository.panels.ViewAttachmentPanel;
import se.su.dsv.scipro.system.Language; import se.su.dsv.scipro.system.Language;
import se.su.dsv.scipro.util.JavascriptEventConfirmation; import se.su.dsv.scipro.util.JavascriptEventConfirmation;
public class FillOutReportPanel<T extends OppositionReport> extends Border { public class FillOutReportPanel extends Border {
public static final String FORM = "form"; public static final String FORM = "form";
public static final String GRADING_CRITERIA = "criteria"; public static final String GRADING_CRITERIA = "criteria";
@ -42,20 +42,20 @@ public class FillOutReportPanel<T extends OppositionReport> extends Border {
public static final String FEEDBACK_PANEL = "feedbackPanel"; public static final String FEEDBACK_PANEL = "feedbackPanel";
@Inject @Inject
private ReportService reportService; private OppositionReportService reportService;
public FillOutReportPanel(String id, final IModel<T> model) { public FillOutReportPanel(String id, final IModel<OppositionReport> model) {
super(id, model); super(id, model);
ReportForm form = new ReportForm(FORM, model); ReportForm form = new ReportForm(FORM, model);
addToBorder(new ScrollingSaveButtonPanel(SAVE, form)); addToBorder(new ScrollingSaveButtonPanel(SAVE, form));
addToBorder(form); addToBorder(form);
} }
private class ReportForm extends StatelessForm<T> { private class ReportForm extends StatelessForm<OppositionReport> {
private final FileUploadField attachment; private final FileUploadField attachment;
public ReportForm(String id, final IModel<T> model) { public ReportForm(String id, final IModel<OppositionReport> model) {
super(id, model); super(id, model);
add(new ComponentFeedbackPanel(FEEDBACK_PANEL, this)); add(new ComponentFeedbackPanel(FEEDBACK_PANEL, this));
IModel<Language> language = model.map(OppositionReport::getLanguage); IModel<Language> language = model.map(OppositionReport::getLanguage);
@ -139,9 +139,9 @@ public class FillOutReportPanel<T extends OppositionReport> extends Border {
} }
} }
private class DeleteAttachmentLink extends Link<T> { private class DeleteAttachmentLink extends Link<OppositionReport> {
public DeleteAttachmentLink(String id, IModel<T> model) { public DeleteAttachmentLink(String id, IModel<OppositionReport> model) {
super(id, model); super(id, model);
add(new JavascriptEventConfirmation("click", new ResourceModel("delete.attachment"))); add(new JavascriptEventConfirmation("click", new ResourceModel("delete.attachment")));
} }

View File

@ -10,6 +10,8 @@ import org.apache.wicket.util.string.StringValueConversionException;
import se.su.dsv.scipro.activityplan.ProjectActivityPlanPage; import se.su.dsv.scipro.activityplan.ProjectActivityPlanPage;
import se.su.dsv.scipro.activityplan.SupervisorActivityPlanPage; import se.su.dsv.scipro.activityplan.SupervisorActivityPlanPage;
import se.su.dsv.scipro.finalseminar.FinalSeminar; import se.su.dsv.scipro.finalseminar.FinalSeminar;
import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition;
import se.su.dsv.scipro.finalseminar.OppositionReportPage;
import se.su.dsv.scipro.finalseminar.ProjectFinalSeminarDetailsPage; import se.su.dsv.scipro.finalseminar.ProjectFinalSeminarDetailsPage;
import se.su.dsv.scipro.finalseminar.ProjectFinalSeminarPage; import se.su.dsv.scipro.finalseminar.ProjectFinalSeminarPage;
import se.su.dsv.scipro.finalseminar.ProjectOppositionPage; import se.su.dsv.scipro.finalseminar.ProjectOppositionPage;
@ -217,6 +219,19 @@ public class NotificationLandingPage extends WebPage {
} else if ( } else if (
seminar.getActiveParticipants().contains(currentUser) || seminar.getOpponents().contains(currentUser) seminar.getActiveParticipants().contains(currentUser) || seminar.getOpponents().contains(currentUser)
) { ) {
if (seminarEvent.getEvent() == SeminarEvent.Event.OPPOSITION_REPORT_IMPROVEMENTS_REQUESTED) {
Optional<FinalSeminarOpposition> opposition = seminar
.getOppositions()
.stream()
.filter(op -> op.getUser().equals(currentUser))
.findFirst();
if (opposition.isPresent()) {
final PageParameters oppPP = new PageParameters();
oppPP.set("oid", opposition.get().getId());
setResponsePage(OppositionReportPage.class, oppPP);
return;
}
}
setResponsePage(ProjectFinalSeminarDetailsPage.class, pp); setResponsePage(ProjectFinalSeminarDetailsPage.class, pp);
} }
} }

View File

@ -83,6 +83,8 @@ SeminarEvent.OPPOSITION_REPORT_UPLOADED = Opposition report created.
SeminarEvent.THESIS_DELETED = Final seminar thesis deleted. SeminarEvent.THESIS_DELETED = Final seminar thesis deleted.
SeminarEvent.THESIS_UPLOAD_REMIND = Authors reminded to upload final seminar thesis. SeminarEvent.THESIS_UPLOAD_REMIND = Authors reminded to upload final seminar thesis.
SeminarEvent.CANCELLED = Final seminar cancelled. SeminarEvent.CANCELLED = Final seminar cancelled.
SeminarEvent.OPPOSITION_REPORT_SUBMITTED = Opposition report submitted.
SeminarEvent.OPPOSITION_REPORT_IMPROVEMENTS_REQUESTED = Opposition report improvements requested.
IdeaEvent.STATUS_CHANGE = Idea status changed. IdeaEvent.STATUS_CHANGE = Idea status changed.
IdeaEvent.PARTNER_ACCEPT = Partner (author) accepted partnering idea. IdeaEvent.PARTNER_ACCEPT = Partner (author) accepted partnering idea.

View File

@ -321,9 +321,6 @@ public abstract class SciProTest {
@Mock @Mock
protected FinalSeminarUploadController finalSeminarUploadController; protected FinalSeminarUploadController finalSeminarUploadController;
@Mock
protected FinalSeminarOppositionRepo finalSeminarOppositionRepo;
@Mock @Mock
protected PlagiarismControl plagiarismControl; protected PlagiarismControl plagiarismControl;

View File

@ -17,7 +17,6 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers; import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
import se.su.dsv.scipro.SciProTest; import se.su.dsv.scipro.SciProTest;
import se.su.dsv.scipro.file.FileDescription; import se.su.dsv.scipro.file.FileDescription;
@ -28,7 +27,6 @@ import se.su.dsv.scipro.project.pages.ProjectDetailsPage;
import se.su.dsv.scipro.report.GradingCriterionPointTemplate; import se.su.dsv.scipro.report.GradingCriterionPointTemplate;
import se.su.dsv.scipro.report.GradingReportTemplate; import se.su.dsv.scipro.report.GradingReportTemplate;
import se.su.dsv.scipro.report.OppositionReport; import se.su.dsv.scipro.report.OppositionReport;
import se.su.dsv.scipro.report.ReportService;
import se.su.dsv.scipro.system.DegreeType; import se.su.dsv.scipro.system.DegreeType;
import se.su.dsv.scipro.system.ProjectType; import se.su.dsv.scipro.system.ProjectType;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
@ -41,9 +39,6 @@ public class OppositionReportPageTest extends SciProTest {
public static final String CRITERION_DESCRIPTION = "For 1 point: Be nice to your supervisor"; public static final String CRITERION_DESCRIPTION = "For 1 point: Be nice to your supervisor";
public static final String CRITERTION_TITLE = "U1 Sammanfattning"; public static final String CRITERTION_TITLE = "U1 Sammanfattning";
@Mock
private ReportService reportService;
private FinalSeminarOpposition finalSeminarOpposition; private FinalSeminarOpposition finalSeminarOpposition;
private User user; private User user;
private ProjectType bachelor; private ProjectType bachelor;
@ -76,10 +71,9 @@ public class OppositionReportPageTest extends SciProTest {
Mockito.when(finalSeminarService.findByProject(opponentsProject)).thenReturn(finalSeminar); Mockito.when(finalSeminarService.findByProject(opponentsProject)).thenReturn(finalSeminar);
} }
private void mockReport(ProjectType bachelor) { private OppositionReport mockReport(ProjectType bachelor) {
GradingReportTemplate reportTemplate = createTemplate(bachelor); GradingReportTemplate reportTemplate = createTemplate(bachelor);
OppositionReport oppositionReport = reportTemplate.createOppositionReport(finalSeminarOpposition); return reportTemplate.createOppositionReport(finalSeminarOpposition);
Mockito.when(oppositionReportService.findOrCreateReport(finalSeminarOpposition)).thenReturn(oppositionReport);
} }
@Test @Test
@ -104,14 +98,16 @@ public class OppositionReportPageTest extends SciProTest {
public void disable_form_if_opposition_does_not_belong_to_logged_in_user() { public void disable_form_if_opposition_does_not_belong_to_logged_in_user() {
mockReport(bachelor); mockReport(bachelor);
long oppositionId = 4L; long oppositionId = 4L;
Mockito.when(finalSeminarOppositionRepo.findOne(oppositionId)).thenReturn(finalSeminarOpposition); Mockito.when(finalSeminarOppositionService.getOpposition(oppositionId)).thenReturn(
new Opposition(user, mockReport(bachelor), Optional.empty())
);
startPage(oppositionId); startPage(oppositionId);
tester.assertDisabled(FILL_OUT_REPORT); tester.assertDisabled(FILL_OUT_REPORT);
} }
@Test @Test
public void redirect_if_no_opposition_is_found_from_id() { public void redirect_if_no_opposition_is_found_from_id() {
Mockito.when(finalSeminarOppositionRepo.findOne(ArgumentMatchers.anyLong())).thenReturn(null); Mockito.when(finalSeminarOppositionService.getOpposition(ArgumentMatchers.anyLong())).thenReturn(null);
startPage(1L); startPage(1L);
tester.assertRenderedPage(ProjectDetailsPage.class); tester.assertRenderedPage(ProjectDetailsPage.class);
} }
@ -138,7 +134,7 @@ public class OppositionReportPageTest extends SciProTest {
formTester.submit(); formTester.submit();
ArgumentCaptor<OppositionReport> captor = ArgumentCaptor.forClass(OppositionReport.class); ArgumentCaptor<OppositionReport> captor = ArgumentCaptor.forClass(OppositionReport.class);
Mockito.verify(reportService).save(captor.capture(), eq(Optional.empty())); Mockito.verify(oppositionReportService).save(captor.capture(), eq(Optional.empty()));
Assertions.assertEquals(summary, captor.getValue().getThesisSummary()); Assertions.assertEquals(summary, captor.getValue().getThesisSummary());
} }
@ -156,7 +152,9 @@ public class OppositionReportPageTest extends SciProTest {
private void startOppositionPage() { private void startOppositionPage() {
long oppositionId = 4L; long oppositionId = 4L;
setLoggedInAs(user); setLoggedInAs(user);
Mockito.when(finalSeminarOppositionRepo.findOne(oppositionId)).thenReturn(finalSeminarOpposition); Mockito.when(finalSeminarOppositionService.getOpposition(oppositionId)).thenReturn(
new Opposition(user, mockReport(bachelor), Optional.empty())
);
startPage(oppositionId); startPage(oppositionId);
} }

View File

@ -61,6 +61,15 @@ public class SeminarOppositionPanelTest extends SciProTest {
finalSeminar.setProject(project); finalSeminar.setProject(project);
setLoggedInAs(supervisorUser); setLoggedInAs(supervisorUser);
Mockito.lenient()
.when(finalSeminarOppositionService.getCriteriaForOpposition(opposition))
.thenReturn(
new OppositionCriteria(
1,
List.of(new OppositionCriteria.Point(0, ""), new OppositionCriteria.Point(1, "Filled in report"))
)
);
} }
@Test @Test

View File

@ -66,6 +66,14 @@ public class SeminarPanelTest extends SciProTest {
Mockito.when(plagiarismControl.getStatus(any(FileDescription.class))).thenReturn( Mockito.when(plagiarismControl.getStatus(any(FileDescription.class))).thenReturn(
new PlagiarismControl.Status.NotSubmitted() new PlagiarismControl.Status.NotSubmitted()
); );
Mockito.lenient()
.when(finalSeminarOppositionService.getCriteriaForOpposition(opposition))
.thenReturn(
new OppositionCriteria(
1,
List.of(new OppositionCriteria.Point(0, ""), new OppositionCriteria.Point(1, "Filled in report"))
)
);
} }
private void addCoSupervisorToProject() { private void addCoSupervisorToProject() {

View File

@ -35,7 +35,7 @@ public class FillOutReportPanelTest extends SciProTest {
private FillOutReportPanel panel; private FillOutReportPanel panel;
@Mock @Mock
private ReportService reportService; private OppositionReportService reportService;
@BeforeEach @BeforeEach
public void setUp() throws Exception { public void setUp() throws Exception {
@ -197,6 +197,6 @@ public class FillOutReportPanelTest extends SciProTest {
} }
private void startPanel() { private void startPanel() {
panel = tester.startComponentInPage(new FillOutReportPanel<>("id", Model.of(oppositionReport))); panel = tester.startComponentInPage(new FillOutReportPanel("id", Model.of(oppositionReport)));
} }
} }

View File

@ -13,6 +13,8 @@ import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.Scope;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import se.su.dsv.scipro.file.FileService; import se.su.dsv.scipro.file.FileService;
import se.su.dsv.scipro.finalseminar.ExpireUnfulfilledOppositionImprovementsWorker;
import se.su.dsv.scipro.finalseminar.FinalSeminarOppositionServiceImpl;
import se.su.dsv.scipro.finalseminar.FinalSeminarService; import se.su.dsv.scipro.finalseminar.FinalSeminarService;
import se.su.dsv.scipro.firstmeeting.FirstMeetingReminderWorker; import se.su.dsv.scipro.firstmeeting.FirstMeetingReminderWorker;
import se.su.dsv.scipro.firstmeeting.FirstMeetingService; import se.su.dsv.scipro.firstmeeting.FirstMeetingService;
@ -150,6 +152,14 @@ public class WorkerConfig {
return new SpringManagedWorkerTransactions(platformTransactionManager); return new SpringManagedWorkerTransactions(platformTransactionManager);
} }
@Bean
public ExpireUnfulfilledOppositionImprovementsWorker.Schedule expireUnfulfilledOppositionImprovementsWorkerSchedule(
Scheduler scheduler,
Provider<ExpireUnfulfilledOppositionImprovementsWorker> worker
) {
return new ExpireUnfulfilledOppositionImprovementsWorker.Schedule(scheduler, worker);
}
@Configuration @Configuration
public static class Workers { public static class Workers {
@ -279,5 +289,12 @@ public class WorkerConfig {
public ExpiredRequestWorker expiredRequestWorker() { public ExpiredRequestWorker expiredRequestWorker() {
return new ExpiredRequestWorker(); return new ExpiredRequestWorker();
} }
@Bean
public ExpireUnfulfilledOppositionImprovementsWorker expireUnfulfilledOppositionImprovementsWorker(
FinalSeminarOppositionServiceImpl finalSeminarOppositionService
) {
return new ExpireUnfulfilledOppositionImprovementsWorker(finalSeminarOppositionService);
}
} }
} }