Merge commit '2ac30fa98041fcbc29a31e9d93132c31bd7358c9' into HEAD

This commit is contained in:
Jenkins 2025-03-18 22:40:09 +01:00
commit d6961627dc
93 changed files with 2013 additions and 689 deletions
.gitignoreDockerfilecheckstyle.xml
core
pom.xml
test-data
view/src
war

1
.gitignore vendored

@ -25,4 +25,5 @@ fitnesse/target/
daisy-integration/target/
war/target/
api/target/
test-data/target/
node_modules/

@ -12,16 +12,19 @@ COPY core/pom.xml core/pom.xml
COPY view/pom.xml view/pom.xml
COPY war/pom.xml war/pom.xml
COPY daisy-integration/pom.xml daisy-integration/pom.xml
COPY test-data/pom.xml test-data/pom.xml
COPY api/src/ api/src/
COPY core/src/ core/src/
COPY view/src/ view/src/
COPY war/src/ war/src/
COPY daisy-integration/src/ daisy-integration/src/
COPY test-data/src/ test-data/src/
RUN ./mvnw package \
--define skipTests \
--activate-profiles branch,DEV \
--define checkstyle.skip=true \
--define skip.npm \
--define skip.installnodenpm

7
checkstyle.xml Normal file

@ -0,0 +1,7 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
</module>

@ -23,32 +23,14 @@
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
<exclusions>
<exclusion>
<groupId>org.glassfish.hk2.external</groupId>
<artifactId>javax.inject</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-jaxb</artifactId>
<exclusions>
<exclusion>
<groupId>org.glassfish.hk2.external</groupId>
<artifactId>javax.inject</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<exclusions>
<exclusion>
<groupId>org.glassfish.hk2.external</groupId>
<artifactId>javax.inject</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
@ -135,12 +117,10 @@
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>4.0.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>

@ -31,6 +31,7 @@ import se.su.dsv.scipro.finalseminar.AuthorRepository;
import se.su.dsv.scipro.finalseminar.FinalSeminarActiveParticipationRepository;
import se.su.dsv.scipro.finalseminar.FinalSeminarActiveParticipationServiceImpl;
import se.su.dsv.scipro.finalseminar.FinalSeminarCreationSubscribers;
import se.su.dsv.scipro.finalseminar.FinalSeminarOppositionGrading;
import se.su.dsv.scipro.finalseminar.FinalSeminarOppositionRepo;
import se.su.dsv.scipro.finalseminar.FinalSeminarOppositionServiceImpl;
import se.su.dsv.scipro.finalseminar.FinalSeminarRepository;
@ -153,8 +154,8 @@ import se.su.dsv.scipro.report.GradingReportServiceImpl;
import se.su.dsv.scipro.report.GradingReportTemplateRepo;
import se.su.dsv.scipro.report.GradingReportTemplateRepoImpl;
import se.su.dsv.scipro.report.OppositionReportRepo;
import se.su.dsv.scipro.report.OppositionReportService;
import se.su.dsv.scipro.report.OppositionReportServiceImpl;
import se.su.dsv.scipro.report.ReportServiceImpl;
import se.su.dsv.scipro.report.SupervisorGradingReportRepository;
import se.su.dsv.scipro.reviewing.DecisionRepository;
import se.su.dsv.scipro.reviewing.FinalSeminarApprovalService;
@ -348,14 +349,16 @@ public class CoreConfig {
ForumPostReadStateRepository readStateRepository,
AbstractThreadRepository threadRepository,
FileService fileService,
EventBus eventBus
EventBus eventBus,
CurrentUser currentUser
) {
return new BasicForumServiceImpl(
forumPostRepository,
readStateRepository,
threadRepository,
fileService,
eventBus
eventBus,
currentUser
);
}
@ -430,8 +433,26 @@ public class CoreConfig {
}
@Bean
public FinalSeminarOppositionServiceImpl finalSeminarOppositionService(Provider<EntityManager> em) {
return new FinalSeminarOppositionServiceImpl(em);
public FinalSeminarOppositionServiceImpl finalSeminarOppositionService(
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
@ -669,13 +690,15 @@ public class CoreConfig {
OppositionReportRepo oppositionReportRepository,
GradingReportTemplateRepo gradingReportTemplateRepository,
FileService fileService,
FinalSeminarOppositionRepo finalSeminarOppositionRepository
FinalSeminarOppositionRepo finalSeminarOppositionRepository,
EventBus eventBus
) {
return new OppositionReportServiceImpl(
oppositionReportRepository,
gradingReportTemplateRepository,
fileService,
finalSeminarOppositionRepository
finalSeminarOppositionRepository,
eventBus
);
}
@ -855,11 +878,6 @@ public class CoreConfig {
);
}
@Bean
public ReportServiceImpl reportService(Provider<EntityManager> em, FileService fileService) {
return new ReportServiceImpl(em, fileService);
}
@Bean
public ResearchAreaServiceImpl researchAreaService(Provider<EntityManager> em) {
return new ResearchAreaServiceImpl(em);
@ -1017,11 +1035,6 @@ public class CoreConfig {
);
}
@Bean
public DataInitializer dataInitializer() {
return new DataInitializer();
}
@Bean
public FinalSeminarActivityHandler finalSeminarActivityHandler(
ActivityPlanFacade activityPlanFacade,

@ -1,5 +1,6 @@
package se.su.dsv.scipro.finalseminar;
import java.util.Objects;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.User;
@ -26,4 +27,17 @@ class AbstractOppositionEvent {
public FinalSeminarOpposition getOpposition() {
return opposition;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AbstractOppositionEvent that = (AbstractOppositionEvent) o;
return Objects.equals(opposition, that.opposition);
}
@Override
public int hashCode() {
return Objects.hashCode(opposition);
}
}

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

@ -8,6 +8,7 @@ import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import java.time.Instant;
import java.util.Objects;
import se.su.dsv.scipro.file.FileReference;
import se.su.dsv.scipro.project.Project;
@ -31,6 +32,14 @@ public class FinalSeminarOpposition extends FinalSeminarParticipation {
@Column(name = "feedback", length = FEEDBACK_LENGTH)
private String feedback;
@Basic
@Column(name = "improvements_requested_at")
private Instant improvementsRequestedAt;
@Basic
@Column(name = "supervisor_improvements_comment")
private String supervisorCommentForImprovements;
// ----------------------------------------------------------------------------------
// JPA-mappings of foreign keys in this table (final_seminar_opposition) referencing
// other tables.
@ -92,6 +101,22 @@ public class FinalSeminarOpposition extends FinalSeminarParticipation {
this.oppositionReport = oppositionReport;
}
public Instant getImprovementsRequestedAt() {
return improvementsRequestedAt;
}
public void setImprovementsRequestedAt(Instant improvementsRequestedAt) {
this.improvementsRequestedAt = improvementsRequestedAt;
}
public String getSupervisorCommentForImprovements() {
return supervisorCommentForImprovements;
}
public void setSupervisorCommentForImprovements(String supervisorCommentsForImprovements) {
this.supervisorCommentForImprovements = supervisorCommentsForImprovements;
}
// ----------------------------------------------------------------------------------
// Methods Common To All Objects
// ----------------------------------------------------------------------------------

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

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

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

@ -1,8 +1,18 @@
package se.su.dsv.scipro.finalseminar;
import java.time.Instant;
import se.su.dsv.scipro.system.GenericService;
public interface FinalSeminarOppositionService extends GenericService<FinalSeminarOpposition, Long> {
@Override
void delete(Long aLong);
public interface FinalSeminarOppositionService {
OppositionCriteria getCriteriaForOpposition(FinalSeminarOpposition opposition);
FinalSeminarOpposition gradeOpponent(FinalSeminarOpposition opposition, int points, String feedback)
throws PointNotValidException;
/**
* @return the deadline by which the improvements must have been submitted
*/
Instant requestImprovements(FinalSeminarOpposition opposition, String supervisorComment);
Opposition getOpposition(long id);
}

@ -1,16 +1,177 @@
package se.su.dsv.scipro.finalseminar;
import com.google.common.eventbus.EventBus;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import java.time.Clock;
import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import se.su.dsv.scipro.misc.DaysService;
import se.su.dsv.scipro.report.OppositionReport;
import se.su.dsv.scipro.report.OppositionReportService;
import se.su.dsv.scipro.system.AbstractServiceImpl;
public class FinalSeminarOppositionServiceImpl
extends AbstractServiceImpl<FinalSeminarOpposition, Long>
implements FinalSeminarOppositionService {
private final FinalSeminarOppositionGrading finalSeminarOppositionGrading;
private final EventBus eventBus;
private final FinalSeminarOppositionRepo finalSeminarOppositionRepository;
private final Clock clock;
private final FinalSeminarSettingsService finalSeminarSettingsService;
private final DaysService daysService;
private final OppositionReportService oppositionReportService;
@Inject
public FinalSeminarOppositionServiceImpl(Provider<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);
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);
}
}
}
}

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

@ -34,6 +34,9 @@ public class FinalSeminarSettings extends DomainObject {
@Column(name = "days_ahead_to_upload_thesis", nullable = false)
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)
private boolean thesisMustBePDF = false;
@ -113,6 +116,17 @@ public class FinalSeminarSettings extends DomainObject {
this.oppositionPriorityDays = oppositionPriorityDays;
}
public int getWorkDaysToFixRequestedImprovementsToOppositionReport() {
return workDaysToFixRequestedImprovementsToOppositionReport;
}
public void setWorkDaysToFixRequestedImprovementsToOppositionReport(
int workDaysToFixRequestedImprovementsToOppositionReport
) {
this.workDaysToFixRequestedImprovementsToOppositionReport =
workDaysToFixRequestedImprovementsToOppositionReport;
}
@Override
public String toString() {
return (

@ -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) {}
}

@ -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) {}
}

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

@ -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 + '}';
}
}

@ -23,4 +23,12 @@ public interface BasicForumService extends Serializable {
ForumThread createThread(String subject);
long countUnreadThreads(List<ForumThread> forumThreadList, User user);
ForumPost getLastPost(ForumThread forumThread);
boolean hasAttachments(ForumThread forumThread);
boolean canDelete(ForumPost forumPost);
void deletePost(ForumPost post);
}

@ -10,6 +10,7 @@ import se.su.dsv.scipro.file.FileService;
import se.su.dsv.scipro.forum.dataobjects.ForumPost;
import se.su.dsv.scipro.forum.dataobjects.ForumPostReadState;
import se.su.dsv.scipro.forum.dataobjects.ForumThread;
import se.su.dsv.scipro.system.CurrentUser;
import se.su.dsv.scipro.system.User;
public class BasicForumServiceImpl implements BasicForumService {
@ -19,6 +20,7 @@ public class BasicForumServiceImpl implements BasicForumService {
private final ForumPostReadStateRepository readStateRepository;
private final FileService fileService;
private final EventBus eventBus;
private final CurrentUser currentUserProvider;
@Inject
public BasicForumServiceImpl(
@ -26,13 +28,15 @@ public class BasicForumServiceImpl implements BasicForumService {
final ForumPostReadStateRepository readStateRepository,
AbstractThreadRepository threadRepository,
final FileService fileService,
final EventBus eventBus
final EventBus eventBus,
final CurrentUser currentUserProvider
) {
this.postRepository = postRepository;
this.readStateRepository = readStateRepository;
this.threadRepository = threadRepository;
this.fileService = fileService;
this.eventBus = eventBus;
this.currentUserProvider = currentUserProvider;
}
@Override
@ -66,7 +70,7 @@ public class BasicForumServiceImpl implements BasicForumService {
@Override
public boolean isThreadRead(User user, ForumThread forumThread) {
for (ForumPost post : forumThread.getPosts()) {
for (ForumPost post : getPosts(forumThread)) {
if (!getReadState(user, post).isRead()) {
return false;
}
@ -133,4 +137,56 @@ public class BasicForumServiceImpl implements BasicForumService {
return post;
}
@Override
public ForumPost getLastPost(ForumThread forumThread) {
return Collections.max(
getPosts(forumThread),
Comparator.comparing(ForumPost::getDateCreated).thenComparing(ForumPost::getId)
);
}
@Override
public boolean hasAttachments(ForumThread forumThread) {
for (ForumPost post : getPosts(forumThread)) {
if (!post.getAttachments().isEmpty()) {
return true;
}
}
return false;
}
@Override
public boolean canDelete(ForumPost forumPost) {
ForumPost initialPost = forumPost.getForumThread().getPosts().get(0);
if (forumPost.equals(initialPost)) {
// The initial post in a thread can never be deleted
return false;
}
User user = currentUserProvider.get();
// Current user can be null meaning the call came from the system
if (user == null) {
// Allow the system to delete any post
return true;
}
return Objects.equals(forumPost.getPostedBy(), user);
}
@Override
@Transactional
public void deletePost(ForumPost post) {
if (!canDelete(post)) {
throw new PostCantBeDeletedException();
}
post.setDeleted(true);
postRepository.save(post);
}
private static final class PostCantBeDeletedException extends IllegalArgumentException {
public PostCantBeDeletedException() {
super("User is not allowed to delete post");
}
}
}

@ -116,13 +116,4 @@ public class ForumThread extends LazyDeletableDomainObject {
public User getCreatedBy() {
return getPosts().get(0).getPostedBy();
}
public boolean hasAttachments() {
for (ForumPost post : posts) {
if (!post.getAttachments().isEmpty()) {
return true;
}
}
return false;
}
}

@ -176,18 +176,16 @@ public class ApplicationPeriod extends DomainObject {
return Collections.unmodifiableSet(answerSet);
}
public void setProjectTypes(Iterable<ProjectType> projectTypes) {
this.projectTypes.clear();
public void setProjectTypes(Set<ProjectType> projectTypes) {
this.projectTypes.removeIf(appt -> !projectTypes.contains(appt.getProjectType()));
for (ProjectType pt : projectTypes) {
this.projectTypes.add(new ApplicationPeriodProjectType(this, pt));
if (this.projectTypes.stream().noneMatch(appt -> appt.getProjectType().equals(pt))) {
addProjectType(pt);
}
}
}
public void addProjectType(ProjectType projectType) {
this.projectTypes.add(new ApplicationPeriodProjectType(this, projectType));
}
public void removeProjectType(ProjectType projectType) {
this.projectTypes.removeIf(next -> next.getProjectType().equals(projectType));
}
}

@ -235,10 +235,8 @@ public class IdeaServiceImpl extends AbstractServiceImpl<Idea, Long> implements
if (authorParticipatingOnActiveIdea(coAuthor, ap)) {
return new Pair<>(Boolean.FALSE, PARTNER_ALREADY_PARTICIPATING_ERROR);
}
if (
coAuthor.getDegreeType() != ProjectType.UNKNOWN &&
coAuthor.getDegreeType() != idea.getProjectType().getDegreeType()
) {
List<ProjectType> typesForCoAuthor = applicationPeriodService.getTypesForStudent(ap, coAuthor);
if (!typesForCoAuthor.contains(idea.getProjectType())) {
return new Pair<>(Boolean.FALSE, WRONG_LEVEL_FOR_YOUR_PARTNER);
}
if (!projectService.getActiveProjectsByUserAndProjectType(coAuthor, idea.getProjectType()).isEmpty()) {

@ -1,6 +1,9 @@
package se.su.dsv.scipro.misc;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
public interface DaysService {
@ -9,4 +12,11 @@ public interface DaysService {
int workDaysBetween(Date startDate, Date endDate);
LocalDate workDaysAhead(LocalDate date, int days);
LocalDate workDaysAfter(LocalDate date, int days);
default Instant workDaysAfter(Instant instant, int days) {
ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault());
LocalDate localDate = zonedDateTime.toLocalDate();
LocalDate newDate = workDaysAfter(localDate, days);
return newDate.atTime(zonedDateTime.toLocalTime()).atZone(ZoneId.systemDefault()).toInstant();
}
}

@ -11,7 +11,9 @@ import se.su.dsv.scipro.finalseminar.FinalSeminarCreatedEvent;
import se.su.dsv.scipro.finalseminar.FinalSeminarDeletedEvent;
import se.su.dsv.scipro.finalseminar.FinalSeminarThesisDeletedEvent;
import se.su.dsv.scipro.finalseminar.FinalSeminarThesisUploadedEvent;
import se.su.dsv.scipro.finalseminar.OppositionApprovedEvent;
import se.su.dsv.scipro.finalseminar.OppositionFailedEvent;
import se.su.dsv.scipro.finalseminar.OppositionReportImprovementsRequestedEvent;
import se.su.dsv.scipro.finalseminar.ParticipationFailedEvent;
import se.su.dsv.scipro.notifications.dataobject.NotificationSource;
import se.su.dsv.scipro.notifications.dataobject.PeerEvent;
@ -23,6 +25,7 @@ import se.su.dsv.scipro.project.ProjectActivatedEvent;
import se.su.dsv.scipro.project.ProjectCompletedEvent;
import se.su.dsv.scipro.project.ProjectDeactivatedEvent;
import se.su.dsv.scipro.project.ReviewerAssignedEvent;
import se.su.dsv.scipro.report.OppositionReportSubmittedEvent;
@Singleton
public class Notifications {
@ -168,6 +171,40 @@ public class Notifications {
);
}
@Subscribe
public void oppositionApproved(OppositionApprovedEvent event) {
notificationController.notifySeminar(
event.getFinalSeminar(),
SeminarEvent.Event.OPPOSITION_APPROVED,
new NotificationSource()
);
}
@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
public void reviewersChanged(ReviewerAssignedEvent event) {
notificationController.notifyProject(

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

@ -140,6 +140,14 @@ 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.
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.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.OPPOSITION_APPROVED.title = Opposition approved
FINAL_SEMINAR.OPPOSITION_APPROVED.body = Your opposition report has been approved by the final seminar supervisor.
FINAL_SEMINAR.compilationSuffix = , project: {0}
PEER.REVIEW_COMPLETED.title = Peer review completed

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

@ -1,6 +1,7 @@
package se.su.dsv.scipro.report;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.time.Clock;
@ -8,6 +9,9 @@ import java.time.Instant;
import java.time.LocalDate;
import java.util.*;
import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition;
import se.su.dsv.scipro.finalseminar.FinalSeminarOppositionGrading;
import se.su.dsv.scipro.finalseminar.OppositionApprovedEvent;
import se.su.dsv.scipro.finalseminar.OppositionCriteria;
import se.su.dsv.scipro.grading.GradingBasis;
import se.su.dsv.scipro.grading.GradingReportTemplateService;
import se.su.dsv.scipro.grading.GradingReportTemplateUpdate;
@ -20,7 +24,8 @@ import se.su.dsv.scipro.system.ProjectTypeService;
import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.util.Either;
public class GradingReportServiceImpl implements GradingReportTemplateService, GradingReportService {
public class GradingReportServiceImpl
implements GradingReportTemplateService, GradingReportService, FinalSeminarOppositionGrading {
private final EventBus eventBus;
private final ThesisSubmissionHistoryService thesisSubmissionHistoryService;
@ -44,11 +49,11 @@ public class GradingReportServiceImpl implements GradingReportTemplateService, G
this.supervisorGradingReportRepository = supervisorGradingReportRepository;
this.gradingReportTemplateRepo = gradingReportTemplateRepo;
this.projectTypeService = projectTypeService;
eventBus.register(this);
}
@Override
@Transactional
public boolean updateOppositionCriteria(SupervisorGradingReport report, FinalSeminarOpposition opposition) {
private boolean updateOppositionCriteria(SupervisorGradingReport report, FinalSeminarOpposition opposition) {
for (GradingCriterion gradingCriterion : report.getIndividualCriteria()) {
boolean isOppositionCriterion = gradingCriterion.getFlag() == GradingCriterion.Flag.OPPOSITION;
boolean betterGrade =
@ -294,4 +299,39 @@ public class GradingReportServiceImpl implements GradingReportTemplateService, G
return gradingReportTemplateRepo.createTemplate(projectType, update);
}
@Subscribe
public void opponentApproved(OppositionApprovedEvent event) {
SupervisorGradingReport report = getSupervisorGradingReport(event.getProject(), event.getStudent());
updateOppositionCriteria(report, event.getOpposition());
}
@Override
@Transactional
public OppositionCriteria oppositionCriteria(FinalSeminarOpposition opposition) {
SupervisorGradingReport supervisorGradingReport = getSupervisorGradingReport(
opposition.getProject(),
opposition.getUser()
);
Optional<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);
}
}

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

@ -1,10 +1,13 @@
package se.su.dsv.scipro.report;
import com.google.common.eventbus.EventBus;
import jakarta.inject.Inject;
import jakarta.inject.Named;
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.finalseminar.FinalSeminarOpposition;
import se.su.dsv.scipro.finalseminar.FinalSeminarOppositionRepo;
@ -15,18 +18,21 @@ public class OppositionReportServiceImpl implements OppositionReportService {
private GradingReportTemplateRepo gradingReportTemplateRepo;
private FileService fileService;
private FinalSeminarOppositionRepo finalSeminarOppositionRepo;
private final EventBus eventBus;
@Inject
public OppositionReportServiceImpl(
OppositionReportRepo oppositionReportRepo,
GradingReportTemplateRepo gradingReportTemplateRepo,
FileService fileService,
FinalSeminarOppositionRepo finalSeminarOppositionRepo
FinalSeminarOppositionRepo finalSeminarOppositionRepo,
EventBus eventBus
) {
this.oppositionReportRepo = oppositionReportRepo;
this.gradingReportTemplateRepo = gradingReportTemplateRepo;
this.fileService = fileService;
this.finalSeminarOppositionRepo = finalSeminarOppositionRepo;
this.eventBus = eventBus;
}
@Override
@ -74,4 +80,36 @@ public class OppositionReportServiceImpl implements OppositionReportService {
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);
}
}
}

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

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

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

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

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

@ -559,7 +559,7 @@
<xs:complexType name="course">
<xs:sequence>
<xs:element name="courseCode" type="xs:string" minOccurs="0">
<xs:element name="courseCode" type="xs:string" minOccurs="1">
</xs:element>
<xs:element name="credits" type="xs:float" minOccurs="1">
</xs:element>
@ -567,6 +567,8 @@
</xs:element>
<xs:element name="level" type="educationalLevel" minOccurs="0">
</xs:element>
<xs:element name="degreeThesisCourse" type="xs:boolean" minOccurs="1">
</xs:element>
<xs:element name="eduInstDesignation" type="xs:string" minOccurs="1">
</xs:element>
</xs:sequence>

@ -1,14 +1,22 @@
package se.su.dsv.scipro.finalseminar;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import jakarta.inject.Inject;
import java.time.LocalDate;
import java.time.Month;
import java.util.Date;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.report.AbstractGradingCriterion;
import se.su.dsv.scipro.report.GradingCriterionPointTemplate;
import se.su.dsv.scipro.report.GradingReportTemplate;
import se.su.dsv.scipro.report.OppositionReport;
import se.su.dsv.scipro.system.DegreeType;
@ -46,6 +54,76 @@ public class FinalSeminarOppositionServiceImplIntegrationTest extends Integratio
assertEquals(0, finalSeminarOppositionService.count());
}
@Test
public void opposition_criteria_are_taken_from_the_grading_report_template() {
FinalSeminarOpposition opposition = createOpposition(finalSeminar, createUser());
createSimpleGradingReportTemplateWithPassFail();
assertEquals(2, finalSeminarOppositionService.getCriteriaForOpposition(opposition).pointsAvailable().size());
}
@Test
public void can_not_grade_outside_criterion() {
FinalSeminarOpposition opposition = createOpposition(finalSeminar, createUser());
createSimpleGradingReportTemplateWithPassFail();
PointNotValidException exception = assertThrows(PointNotValidException.class, () ->
finalSeminarOppositionService.gradeOpponent(opposition, 2, "Feedback")
);
assertEquals(2, exception.givenValue());
assertThat(exception.acceptableValues(), hasSize(2));
assertThat(exception.acceptableValues(), contains(0, 1));
}
@Test
public void publishes_failed_event_when_grading_with_failing_criterion() throws Exception {
FinalSeminarOpposition opposition = createOpposition(finalSeminar, createUser());
createSimpleGradingReportTemplateWithPassFail();
finalSeminarOppositionService.gradeOpponent(opposition, 0, "Feedback");
assertThat(getPublishedEvents(), hasItem(new OppositionFailedEvent(opposition)));
}
@Test
public void publishes_approved_event_when_grading_with_passing_criterion() throws Exception {
FinalSeminarOpposition opposition = createOpposition(finalSeminar, createUser());
createSimpleGradingReportTemplateWithPassFail();
finalSeminarOppositionService.gradeOpponent(opposition, 1, "Feedback");
assertThat(getPublishedEvents(), hasItem(new OppositionApprovedEvent(opposition)));
}
@Test
public void stores_assessment() throws Exception {
FinalSeminarOpposition opposition = createOpposition(finalSeminar, createUser());
createSimpleGradingReportTemplateWithPassFail();
FinalSeminarOpposition graded = finalSeminarOppositionService.gradeOpponent(opposition, 1, "Feedback");
assertEquals(1, graded.getPoints());
assertEquals("Feedback", graded.getFeedback());
}
private void createSimpleGradingReportTemplateWithPassFail() {
GradingReportTemplate gradingReportTemplate = createGradingReportTemplate();
GradingCriterionPointTemplate failingCriterion = new GradingCriterionPointTemplate();
failingCriterion.setPoint(0);
GradingCriterionPointTemplate passingCriterion = new GradingCriterionPointTemplate();
passingCriterion.setPoint(1);
gradingReportTemplate.addIndividualCriterion(
"Criterion 1",
"Criterion 1",
1,
List.of(failingCriterion, passingCriterion),
AbstractGradingCriterion.Flag.OPPOSITION
);
save(gradingReportTemplate);
}
private void createOppositionReport(FinalSeminarOpposition opposition) {
OppositionReport report = new OppositionReport(createGradingReportTemplate(), opposition);
opposition.setOppositionReport(report);
@ -93,7 +171,7 @@ public class FinalSeminarOppositionServiceImplIntegrationTest extends Integratio
FinalSeminarOpposition opposition = new FinalSeminarOpposition();
opposition.setFinalSeminar(finalSeminar);
opposition.setUser(student);
opposition.setProject(createProject(createProjectType()));
opposition.setProject(createProject(projectType));
return save(opposition);
}
}

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

@ -121,6 +121,8 @@ public class BasicForumServiceImplTest {
ForumThread forumThread = new ForumThread();
forumThread.addPost(post);
when(postRepository.findByThread(forumThread)).thenReturn(List.of(post));
when(readStateRepository.find(eq(goodUser), isA(ForumPost.class))).thenReturn(readState);
when(readStateRepository.find(eq(badUser), isA(ForumPost.class))).thenReturn(notReadState);

@ -0,0 +1,83 @@
package se.su.dsv.scipro.forum;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import jakarta.inject.Inject;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import se.su.dsv.scipro.forum.dataobjects.ForumPost;
import se.su.dsv.scipro.forum.dataobjects.ForumThread;
import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.test.IntegrationTest;
public class BasicForumServiceIntegrationTest extends IntegrationTest {
@Inject
BasicForumService basicForumService;
private User op;
private User commenter;
@BeforeEach
public void setUp() {
User op = User.builder().firstName("Bill").lastName("Gates").emailAddress("bill@example.com").build();
this.op = save(op);
User commenter = User.builder().firstName("Steve").lastName("Jobs").emailAddress("steve@example.com").build();
this.commenter = save(commenter);
}
@Test
public void can_not_delete_original_post() {
ForumThread thread = basicForumService.createThread("Test thread");
ForumPost originalPost = basicForumService.createReply(thread, op, "Test post", Set.of());
setLoggedInAs(op);
assertFalse(basicForumService.canDelete(originalPost));
assertThrows(IllegalArgumentException.class, () -> basicForumService.deletePost(originalPost));
}
@Test
public void can_delete_reply_to_original_post() {
ForumThread thread = basicForumService.createThread("Test thread");
ForumPost originalPost = basicForumService.createReply(thread, op, "Test post", Set.of());
ForumPost reply = basicForumService.createReply(thread, commenter, "Test reply", Set.of());
setLoggedInAs(commenter);
assertTrue(basicForumService.canDelete(reply));
assertDoesNotThrow(() -> basicForumService.deletePost(reply));
}
@Test
public void can_not_delete_someone_elses_reply() {
ForumThread thread = basicForumService.createThread("Test thread");
ForumPost originalPost = basicForumService.createReply(thread, op, "Test post", Set.of());
ForumPost reply = basicForumService.createReply(thread, commenter, "Test reply", Set.of());
setLoggedInAs(op);
assertFalse(basicForumService.canDelete(reply));
assertThrows(IllegalArgumentException.class, () -> basicForumService.deletePost(reply));
}
@Test
public void system_can_delete_all_replies() {
ForumThread thread = basicForumService.createThread("Test thread");
ForumPost originalPost = basicForumService.createReply(thread, op, "Test post", Set.of());
ForumPost reply = basicForumService.createReply(thread, commenter, "Test reply", Set.of());
ForumPost secondReply = basicForumService.createReply(thread, op, "Test post", Set.of());
setLoggedInAs(null);
assertTrue(basicForumService.canDelete(reply));
assertDoesNotThrow(() -> basicForumService.deletePost(reply));
assertTrue(basicForumService.canDelete(secondReply));
assertDoesNotThrow(() -> basicForumService.deletePost(secondReply));
}
}

@ -241,6 +241,7 @@ public class IdeaServiceImplTest {
when(generalSystemSettingsService.getGeneralSystemSettingsInstance()).thenReturn(new GeneralSystemSettings());
Idea idea = createBachelorIdea(Idea.Status.UNMATCHED);
when(applicationPeriodService.getTypesForStudent(applicationPeriod, student)).thenReturn(List.of(bachelor));
when(applicationPeriodService.getTypesForStudent(applicationPeriod, coAuthor)).thenReturn(List.of(bachelor));
Pair<Boolean, String> acceptance = ideaService.validateStudentAcceptance(
idea,
@ -401,6 +402,39 @@ public class IdeaServiceImplTest {
assertEquals(expected, ideaService.countAuthorsByApplicationPeriod(applicationPeriod, params));
}
@Test
public void wrong_type_for_author() {
when(applicationPeriodService.getTypesForStudent(applicationPeriod, student)).thenReturn(List.of(master));
when(applicationPeriodService.getTypesForStudent(applicationPeriod, coAuthor)).thenReturn(List.of(bachelor));
assertPair(
false,
"The idea is the wrong level for you, please pick another one.",
ideaService.validateStudentAcceptance(
createBachelorIdea(Idea.Status.UNMATCHED),
student,
coAuthor,
applicationPeriod
)
);
}
@Test
public void wrong_type_for_partner() {
when(applicationPeriodService.getTypesForStudent(applicationPeriod, coAuthor)).thenReturn(List.of(master));
assertPair(
false,
"The idea is the wrong level for your partner, please pick another one.",
ideaService.validateStudentAcceptance(
createBachelorIdea(Idea.Status.UNMATCHED),
student,
coAuthor,
applicationPeriod
)
);
}
private Idea mockInactiveIdea() {
Idea idea = new Idea();
Match match = new Match();

@ -5,7 +5,12 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.google.common.eventbus.EventBus;
import com.sun.net.httpserver.HttpServer;
import jakarta.inject.Inject;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.Month;
import java.util.*;
@ -13,6 +18,10 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import se.su.dsv.scipro.finalseminar.FinalSeminar;
import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition;
import se.su.dsv.scipro.finalseminar.OppositionApprovedEvent;
import se.su.dsv.scipro.grading.GetGradeError;
import se.su.dsv.scipro.grading.GradingServiceImpl;
import se.su.dsv.scipro.grading.Result;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.security.auth.roles.Roles;
import se.su.dsv.scipro.system.DegreeType;
@ -31,6 +40,9 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest {
@Inject
private GradingReportServiceImpl gradingReportService;
@Inject
private EventBus eventBus;
private ProjectType projectType;
private GradingReportTemplate gradingReportTemplate;
private Project project;
@ -45,7 +57,6 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest {
project = createProject(projectType, 30);
gradingReportTemplate = createProjectGradingCriterion(gradingReportTemplate, 2);
gradingReportTemplate = createIndividualGradingCriterion(gradingReportTemplate, 2);
gradingReport = createGradingReport(project, student);
}
@Test
@ -68,6 +79,7 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest {
@Test
public void submit_supervisor_grading_report_flags_report_as_submitted() {
gradingReport = createGradingReport(project, student);
assessAllCriteria(gradingReport);
Either<List<SubmissionError>, SupervisorGradingReport> result = gradingReportService.submitReport(
gradingReport
@ -77,6 +89,7 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest {
@Test
public void submitting_supervisor_report_throws_exception_if_report_is_not_finished() {
gradingReport = createGradingReport(project, student);
Either<List<SubmissionError>, SupervisorGradingReport> result = gradingReportService.submitReport(
gradingReport
);
@ -86,38 +99,35 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest {
@Test
public void update_opposition_criterion() {
addOppositionCriterion();
boolean updated = updateOppositionCriterion();
updateOppositionCriterion();
GradingCriterion oppositionCriterion = findOppositionCriterion();
assert oppositionCriterion != null;
assertEquals(FEEDBACK_ON_OPPOSITION, oppositionCriterion.getFeedback());
assertEquals((Integer) OPPOSITION_CRITERION_POINTS, oppositionCriterion.getPoints());
assertTrue(updated);
}
@Test
public void update_opposition_if_title_matches_english_title() {
addOppositionCriterion();
boolean updated = updateOppositionCriterion();
updateOppositionCriterion();
GradingCriterion oppositionCriterion = findEnglishOppositionCriterion("Ö1 Opposition report");
assert oppositionCriterion != null;
assertEquals(FEEDBACK_ON_OPPOSITION, oppositionCriterion.getFeedback());
assertEquals((Integer) OPPOSITION_CRITERION_POINTS, oppositionCriterion.getPoints());
assertTrue(updated);
}
@Test
public void updating_opposition_criterion_does_nothing_if_criterion_already_has_values() {
addOppositionCriterion();
assessAllCriteria(gradingReport);
boolean updated = updateOppositionCriterion();
updateOppositionCriterion();
GradingCriterion oppositionCriterion = findOppositionCriterion();
assert oppositionCriterion != null;
assertEquals(FEEDBACK, oppositionCriterion.getFeedback());
assertEquals((Integer) oppositionCriterion.getMaxPoints(), oppositionCriterion.getPoints());
assertFalse(updated);
}
@Test
@ -146,14 +156,52 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest {
assertNull(oppositionCriterion.getFeedback());
}
@Test
public void test_json_deserialization() throws IOException {
String json =
"""
{
"grade": "A",
"reported": "2021-01-01"
}
""";
HttpServer httpServer = startHttpServerWithJsonResponse(json);
int port = httpServer.getAddress().getPort();
GradingServiceImpl gradingService = new GradingServiceImpl("http://localhost:" + port);
Either<GetGradeError, Optional<Result>> result = gradingService.getResult("token", 1, 2, 3);
Optional<Result> right = result.right();
assertTrue(right.isPresent());
assertEquals(LocalDate.of(2021, 1, 1), right.get().reported());
httpServer.stop(0);
}
private static HttpServer startHttpServerWithJsonResponse(String json) throws IOException {
HttpServer httpServer = HttpServer.create();
httpServer.createContext("/", exchange -> {
try (exchange) {
byte[] response = json.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("Content-Type", "application/json");
exchange.sendResponseHeaders(200, response.length);
exchange.getResponseBody().write(response);
}
});
httpServer.bind(new InetSocketAddress("localhost", 0), 0);
httpServer.start();
return httpServer;
}
private void addOppositionCriterion() {
gradingReportTemplate = createOppositionCriteria(gradingReportTemplate, 2);
gradingReport = createGradingReport(project, student);
}
private boolean updateOppositionCriterion() {
private void updateOppositionCriterion() {
FinalSeminarOpposition opposition = createFinalSeminarOpposition();
return gradingReportService.updateOppositionCriteria(gradingReport, opposition);
eventBus.post(new OppositionApprovedEvent(opposition));
}
private GradingCriterion findOppositionCriterion() {
@ -176,8 +224,8 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest {
private FinalSeminarOpposition createFinalSeminarOpposition() {
FinalSeminarOpposition finalSeminarOpposition = new FinalSeminarOpposition();
finalSeminarOpposition.setProject(createProject(projectType, 30));
finalSeminarOpposition.setUser(createStudent());
finalSeminarOpposition.setProject(project);
finalSeminarOpposition.setUser(student);
finalSeminarOpposition.setFinalSeminar(createFinalSeminar());
finalSeminarOpposition.setFeedback(FEEDBACK_ON_OPPOSITION);

@ -1,11 +1,15 @@
package se.su.dsv.scipro.test;
import com.google.common.eventbus.EventBus;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;
import java.sql.SQLException;
import java.time.Clock;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.flywaydb.core.Flyway;
@ -25,6 +29,7 @@ import se.su.dsv.scipro.RepositoryConfiguration;
import se.su.dsv.scipro.profiles.CurrentProfile;
import se.su.dsv.scipro.sukat.Sukat;
import se.su.dsv.scipro.system.CurrentUser;
import se.su.dsv.scipro.system.User;
@Testcontainers
public abstract class SpringTest {
@ -35,6 +40,11 @@ public abstract class SpringTest {
@Container
static MariaDBContainer<?> mariaDBContainer = new MariaDBContainer<>("mariadb:10.11");
@Inject
private TestUser testUser;
private CapturingEventBus capturingEventBus;
@BeforeEach
public final void prepareSpring() throws SQLException {
MariaDbDataSource dataSource = new MariaDbDataSource(mariaDBContainer.getJdbcUrl());
@ -50,12 +60,17 @@ public abstract class SpringTest {
transaction.begin();
transaction.setRollbackOnly();
capturingEventBus = new CapturingEventBus();
AnnotationConfigApplicationContext annotationConfigApplicationContext =
new AnnotationConfigApplicationContext();
annotationConfigApplicationContext.registerBean("eventBus", EventBus.class, () -> this.capturingEventBus);
annotationConfigApplicationContext.register(TestContext.class);
annotationConfigApplicationContext.getBeanFactory().registerSingleton("entityManager", this.entityManager);
annotationConfigApplicationContext.refresh();
annotationConfigApplicationContext.getAutowireCapableBeanFactory().autowireBean(this);
testUser.setUser(null); // default to system
}
@AfterEach
@ -75,6 +90,14 @@ public abstract class SpringTest {
}
}
protected void setLoggedInAs(User user) {
this.testUser.setUser(user);
}
protected List<Object> getPublishedEvents() {
return capturingEventBus.publishedEvents;
}
@Configuration(proxyBeanMethods = false)
@Import({ CoreConfig.class, RepositoryConfiguration.class })
public static class TestContext {
@ -96,7 +119,7 @@ public abstract class SpringTest {
@Bean
public CurrentUser currentUser() {
return () -> null;
return new TestUser();
}
@Bean
@ -106,4 +129,29 @@ public abstract class SpringTest {
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);
}
}
private static class TestUser implements CurrentUser {
private User user;
@Override
public User get() {
return user;
}
private void setUser(User user) {
this.user = user;
}
}
}

174
pom.xml

@ -15,6 +15,7 @@
<module>daisy-integration</module>
<module>war</module>
<module>api</module>
<module>test-data</module>
</modules>
<properties>
@ -22,23 +23,10 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- Dependency versions -->
<slf4j.version>2.0.7</slf4j.version>
<log4j2.version>2.20.0</log4j2.version>
<wicket.version>10.4.0</wicket.version>
<!-- See https://hibernate.org/orm/releases/ for which version Hibernate implements -->
<jakarta.persistence-api.version>3.1.0</jakarta.persistence-api.version>
<hibernate.version>6.5.0.Final</hibernate.version>
<mariadb-java-client.version>3.2.0</mariadb-java-client.version>
<querydsl.version>5.0.0</querydsl.version>
<jakarta.servlet.version>5.0.0</jakarta.servlet.version>
<junit.version>5.9.3</junit.version>
<mockito.version>5.3.1</mockito.version>
<flyway.version>9.19.1</flyway.version>
<jersey.version>3.1.6</jersey.version>
<poi.version>5.2.5</poi.version>
<jackson.version>2.17.0</jackson.version>
<poi.version>5.4.0</poi.version>
<!--
When updating spring-boot check if the transitive dependency on json-smart has been
@ -113,47 +101,6 @@
<type>pom</type>
</dependency>
<!-- Servlet API, needed for compilation. -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>${jakarta.servlet.version}</version>
<scope>provided</scope>
</dependency>
<!-- LOGGING DEPENDENCIES -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-bom</artifactId>
<version>${log4j2.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>${mariadb-java-client.version}</version>
</dependency>
<!--QueryDSL-->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-bom</artifactId>
<version>${querydsl.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
@ -161,41 +108,6 @@
<classifier>jakarta</classifier>
</dependency>
<!-- JPA -->
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>${jakarta.persistence-api.version}</version>
</dependency>
<!-- Hibernate impl -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Jersey/Jax-Rs -->
<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey</groupId>
<artifactId>jersey-bom</artifactId>
<version>${jersey.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>${jackson.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Additional dependencies -->
<dependency>
<groupId>com.google.guava</groupId>
@ -203,22 +115,6 @@
<version>32.0.1-jre</version>
</dependency>
<dependency>
<groupId>jakarta.mail</groupId>
<artifactId>jakarta.mail-api</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>jakarta.activation</groupId>
<artifactId>jakarta.activation-api</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>org.eclipse.angus</groupId>
<artifactId>jakarta.mail</artifactId>
<version>2.0.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<!--
2.5.1 is brought in transitively by
@ -234,32 +130,6 @@
<version>2.5.2</version>
</dependency>
<!-- Test stuff -->
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>${junit.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
@ -270,16 +140,6 @@
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>${flyway.version}</version>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
<version>${flyway.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
@ -295,13 +155,6 @@
<artifactId>slf4j-api</artifactId>
</dependency>
<!-- Additional dependencies -->
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<!-- Test stuff -->
<dependency>
<groupId>org.junit.jupiter</groupId>
@ -326,7 +179,6 @@
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
@ -359,6 +211,11 @@
<artifactId>maven-war-plugin</artifactId>
<version>3.4.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.6.0</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
@ -460,6 +317,23 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<configuration>
<configLocation>checkstyle.xml</configLocation>
<failOnViolation>true</failOnViolation>
</configuration>
<executions>
<execution>
<id>validate</id>
<phase>process-sources</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

20
test-data/pom.xml Normal file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>se.su.dsv.scipro</groupId>
<artifactId>SciPro</artifactId>
<version>0.1-SNAPSHOT</version>
</parent>
<artifactId>test-data</artifactId>
<dependencies>
<dependency>
<groupId>se.su.dsv.scipro</groupId>
<artifactId>core</artifactId>
</dependency>
</dependencies>
</project>

@ -0,0 +1,29 @@
package se.su.dsv.scipro.testdata;
import java.util.List;
import se.su.dsv.scipro.match.Keyword;
import se.su.dsv.scipro.system.ProjectType;
import se.su.dsv.scipro.system.ResearchArea;
/// All the base test data that can be re-used in different test cases.
///
/// **Do not modify any of this data.** There are many
/// [TestDataPopulator]s that rely on this data to be in a specific state.
///
/// In addition to the data that is available here there is also much additional
/// data that has been created;
///
/// - A grading report template for each [ProjectType]
///
public interface BaseData {
ProjectType bachelor();
ProjectType magister();
ProjectType master();
/**
* @return generic research area with some keywords attached to it
*/
ResearchAreaAndKeywords researchArea();
record ResearchAreaAndKeywords(ResearchArea researchArea, List<Keyword> keywords) {}
}

@ -1,4 +1,4 @@
package se.su.dsv.scipro;
package se.su.dsv.scipro.testdata;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
@ -9,18 +9,35 @@ import java.io.InputStream;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Month;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.function.Function;
import se.su.dsv.scipro.checklist.ChecklistCategory;
import se.su.dsv.scipro.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.match.*;
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.Idea;
import se.su.dsv.scipro.match.IdeaService;
import se.su.dsv.scipro.match.Keyword;
import se.su.dsv.scipro.match.Target;
import se.su.dsv.scipro.match.TholanderBox;
import se.su.dsv.scipro.milestones.dataobjects.MilestoneActivityTemplate;
import se.su.dsv.scipro.milestones.dataobjects.MilestonePhaseTemplate;
import se.su.dsv.scipro.milestones.service.MilestoneActivityTemplateService;
import se.su.dsv.scipro.notifications.dataobject.CustomEvent;
import se.su.dsv.scipro.notifications.dataobject.GroupEvent;
import se.su.dsv.scipro.notifications.dataobject.IdeaEvent;
import se.su.dsv.scipro.notifications.dataobject.MileStoneEvent;
import se.su.dsv.scipro.notifications.dataobject.Notification;
import se.su.dsv.scipro.notifications.dataobject.PeerEvent;
import se.su.dsv.scipro.notifications.dataobject.ProjectEvent;
import se.su.dsv.scipro.notifications.dataobject.ProjectForumEvent;
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.Profiles;
import se.su.dsv.scipro.project.Project;
@ -34,13 +51,16 @@ import se.su.dsv.scipro.security.auth.roles.Roles;
import se.su.dsv.scipro.system.*;
import se.su.dsv.scipro.util.Pair;
public class DataInitializer implements Lifecycle {
public class DataInitializer implements Lifecycle, BaseData, Factory {
public static final int APPLICATION_PERIOD_START_MINUS_DAYS = 1;
public static final int APPLICATION_PERIOD_END_PLUS_DAYS = 3;
public static final int APPLICATION_PERIOD_COURSE_START_PLUS_DAYS = 5;
public static final long RESEARCH_AREA_ID = 12L;
@Inject
private Optional<Collection<TestDataPopulator>> testDataPopulators = Optional.empty();
@Inject
private UserService userService;
@ -53,12 +73,18 @@ public class DataInitializer implements Lifecycle {
@Inject
private MilestoneActivityTemplateService milestoneActivityTemplateService;
@Inject
private FileService fileService;
@Inject
private CurrentProfile profile;
@Inject
private Provider<EntityManager> em;
@Inject
private ReceiverConfigurationService receiverConfigurationService;
@Inject
private RoughDraftApprovalService roughDraftApprovalService;
@ -100,6 +126,7 @@ public class DataInitializer implements Lifecycle {
private ProjectType masterClass;
private ProjectType magisterClass;
private ApplicationPeriod applicationPeriod;
private Project project1;
private Project project2;
@Transactional
@ -120,6 +147,12 @@ public class DataInitializer implements Lifecycle {
createTarget();
createStudentIdea();
createRoughDraftApproval();
createPastFinalSeminar();
setUpNotifications();
Collection<TestDataPopulator> availablePopulators = testDataPopulators.orElseGet(Collections::emptySet);
for (TestDataPopulator testDataPopulator : availablePopulators) {
testDataPopulator.populate(this, this);
}
}
if (profile.getCurrentProfile() == Profiles.DEV && noAdminUser()) {
createAdmin();
@ -136,6 +169,47 @@ public class DataInitializer implements Lifecycle {
reviewerAssignmentService.assignReviewer(project2, eric_employee);
}
private void setUpNotifications() {
enableAllNotifications(Notification.Type.PEER, PeerEvent.Event.values());
enableAllNotifications(Notification.Type.FORUM, ProjectForumEvent.Event.values());
enableAllNotifications(Notification.Type.GROUP, GroupEvent.Event.values());
enableAllNotifications(Notification.Type.MILESTONE, MileStoneEvent.Event.values());
enableAllNotifications(Notification.Type.PROJECT, ProjectEvent.Event.values());
enableAllNotifications(Notification.Type.IDEA, IdeaEvent.Event.values());
enableAllNotifications(Notification.Type.CUSTOM, CustomEvent.Event.values());
enableAllNotifications(Notification.Type.FINAL_SEMINAR, SeminarEvent.Event.values());
}
private void enableAllNotifications(Notification.Type type, Enum<?>[] events) {
for (Enum<?> event : events) {
for (Member.Type member : Member.Type.values()) {
receiverConfigurationService.setReceiving(type, event, member, 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
public void stop() {}
@ -195,7 +269,7 @@ public class DataInitializer implements Lifecycle {
}
private void createProjects() {
createProject(PROJECT_1, eric_employee, sture_student, stina_student, eve_employee, 135);
project1 = createProject(PROJECT_1, eric_employee, sture_student, stina_student, eve_employee, 135);
project2 = createProject(PROJECT_2, eve_employee, sid_student, simon_student, eric_employee, 246);
}
@ -243,13 +317,18 @@ public class DataInitializer implements Lifecycle {
sofia_student = createStudent("Sofia", 17);
}
private User createStudent(String firstName, int identifier) {
private User createStudent(String firstName) {
User user = createUser(firstName, STUDENT_LAST);
user.setIdentifier(identifier);
createBeta(user);
return user;
}
private User createStudent(String firstName, int identifier) {
User user = createStudent(firstName);
user.setIdentifier(identifier);
return user;
}
private User createEmployee(String firstName) {
User user = createUser(firstName, EMPLOYEE_LAST);
Unit u = createUnit();
@ -1904,9 +1983,6 @@ public class DataInitializer implements Lifecycle {
magisterClass = new ProjectType(ProjectType.MAGISTER, "Magister", "One-year-Master degree thesis project");
save(magisterClass);
final ProjectType phdClass = new ProjectType(DegreeType.NONE, "PhD", "PhD project");
save(phdClass);
}
private void createDefaultChecklistCategoriesIfNotDone() {
@ -2087,6 +2163,41 @@ public class DataInitializer implements Lifecycle {
return entity;
}
@Override
public ProjectType bachelor() {
return bachelorClass;
}
@Override
public ProjectType magister() {
return magisterClass;
}
@Override
public ProjectType master() {
return masterClass;
}
@Override
public User createAuthor(String firstName) {
return createStudent(firstName);
}
@Override
public User createSupervisor(String firstName) {
return createEmployee(firstName);
}
@Override
public User createReviewer(String firstName) {
return createEmployee(firstName);
}
@Override
public ResearchAreaAndKeywords researchArea() {
return new ResearchAreaAndKeywords(researchArea1, List.of(keyword1, keyword2));
}
private static final class SimpleTextFile implements FileUpload {
private final User uploader;

@ -0,0 +1,46 @@
package se.su.dsv.scipro.testdata;
import se.su.dsv.scipro.security.auth.roles.Roles;
import se.su.dsv.scipro.system.User;
/**
* A factory to help with repetitive tasks when populating test data.
*/
public interface Factory {
/**
* Creates a user with the given first name and last name "Student".
* The user is given the role {@link Roles#AUTHOR}.
* <p>
* A username is created of the form {@code <first_name>@example.com} that
* can be used to log in.
*/
User createAuthor(String firstName);
/**
* Creates a user with the given first name and last name "Employee".
* <p>
* The user is given the role {@link Roles#SUPERVISOR}, {@link Roles#REVIEWER},
* and {@link Roles#EXAMINER}.
* <p>
* The user gets a default research area, unit, and language. It is also
* marked as {@link User#setActiveAsSupervisor(boolean) an active supervisor}.
* <p>
* A username is created of the form {@code <first_name>@example.com} that
* can be used to log in.
*/
User createSupervisor(String firstName);
/**
* Creates a user with the given first name and last name "Employee".
* <p>
* The user is given the role {@link Roles#SUPERVISOR}, {@link Roles#REVIEWER},
* and {@link Roles#EXAMINER}.
* <p>
* The user gets a default research area, unit, and language. It is also
* marked as {@link User#setActiveAsSupervisor(boolean) an active supervisor}.
* <p>
* A username is created of the form {@code <first_name>@example.com} that
* can be used to log in.
*/
User createReviewer(String firstName);
}

@ -0,0 +1,16 @@
package se.su.dsv.scipro.testdata;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import se.su.dsv.scipro.war.PluginConfiguration;
@Configuration(proxyBeanMethods = false)
@ComponentScan(basePackages = "se.su.dsv.scipro.testdata.populators")
public class TestDataConfiguration implements PluginConfiguration {
@Bean
public DataInitializer dataInitializer() {
return new DataInitializer();
}
}

@ -0,0 +1,11 @@
package se.su.dsv.scipro.testdata;
public interface TestDataPopulator {
/**
* Add test data to the system to help with testing a specific feature.
*
* @param baseData the base data already populated
* @param factory helper object to make repetitive tasks easier (such as creating users)
*/
void populate(BaseData baseData, Factory factory);
}

@ -0,0 +1,8 @@
/**
* This package contains the infrastructure that is used when generating test data for the application. To add new test
* data to the system, add a new class to the {@link se.su.dsv.scipro.testdata.populators} package that implements the
* {@link se.su.dsv.scipro.testdata.TestDataPopulator} interface and annotate it with
* {@link org.springframework.stereotype.Service @Service}. Inject dependencies as needed using
* {@link jakarta.inject.Inject @Inject}.
*/
package se.su.dsv.scipro.testdata;

@ -0,0 +1,65 @@
package se.su.dsv.scipro.testdata.populators;
import jakarta.inject.Inject;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import org.springframework.stereotype.Service;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.project.ProjectService;
import se.su.dsv.scipro.system.ProjectType;
import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.testdata.BaseData;
import se.su.dsv.scipro.testdata.Factory;
import se.su.dsv.scipro.testdata.TestDataPopulator;
@Service
public class GroupCreationUXImprovement implements TestDataPopulator {
private static final String[] STUDENT_NAMES = { "Alice", "Bob", "Charlie", "David", "Emma" };
private final ProjectService projectService;
@Inject
public GroupCreationUXImprovement(ProjectService projectService) {
this.projectService = projectService;
}
@Override
public void populate(BaseData baseData, Factory factory) {
User supervisor = factory.createSupervisor("Evan");
List<User> students = createStudents(factory);
for (int i = 1; i <= 20; i++) {
projectService.save(createProject(baseData, i, supervisor, students));
}
}
private List<User> createStudents(Factory factory) {
return Arrays.stream(STUDENT_NAMES).map(factory::createAuthor).toList();
}
private Project createProject(BaseData baseData, int i, User supervisor, List<User> students) {
User author1 = students.get(i % students.size());
User author2 = students.get((i + 1) % students.size());
String title = "Test project " + i;
if (i % 6 == 0) {
title = title + " with a very long title that makes the project special";
}
ProjectType projectType =
switch (i % 3) {
case 1 -> baseData.magister();
case 2 -> baseData.master();
default -> baseData.bachelor();
};
return Project.builder()
.title(title)
.projectType(projectType)
.startDate(LocalDate.now())
.headSupervisor(supervisor)
.projectParticipants(Set.of(author1, author2))
.build();
}
}

@ -0,0 +1,68 @@
package se.su.dsv.scipro.testdata.populators;
import jakarta.inject.Inject;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Set;
import org.springframework.stereotype.Service;
import se.su.dsv.scipro.match.ApplicationPeriod;
import se.su.dsv.scipro.match.ApplicationPeriodService;
import se.su.dsv.scipro.match.Idea;
import se.su.dsv.scipro.match.IdeaService;
import se.su.dsv.scipro.match.Target;
import se.su.dsv.scipro.match.TargetService;
import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.testdata.BaseData;
import se.su.dsv.scipro.testdata.Factory;
import se.su.dsv.scipro.testdata.TestDataPopulator;
@Service
public class PartnerTypeExemption implements TestDataPopulator {
private final ApplicationPeriodService applicationPeriodService;
private final IdeaService ideaService;
private final TargetService targetService;
@Inject
public PartnerTypeExemption(
ApplicationPeriodService applicationPeriodService,
IdeaService ideaService,
TargetService targetService
) {
this.applicationPeriodService = applicationPeriodService;
this.ideaService = ideaService;
this.targetService = targetService;
}
@Override
public void populate(BaseData baseData, Factory factory) {
factory.createAuthor("Oskar");
User johan = factory.createAuthor("Johan");
johan.setDegreeType(baseData.master().getDegreeType());
User supervisor = factory.createSupervisor("Elsa");
ApplicationPeriod applicationPeriod = new ApplicationPeriod("Supervisor ideas");
applicationPeriod.setStartDate(LocalDate.now());
applicationPeriod.setEndDate(LocalDate.now().plusDays(14));
applicationPeriod.setCourseStartDateTime(LocalDateTime.now().plusDays(15));
applicationPeriod.setProjectTypes(Set.of(baseData.bachelor()));
applicationPeriodService.save(applicationPeriod);
Target target = targetService.findOne(applicationPeriod, supervisor, baseData.bachelor());
target.setTarget(10);
targetService.save(target);
Idea idea = new Idea();
idea.setPublished(true);
idea.setTitle("The next gen AI 2.0 turbo edition");
idea.setPrerequisites("Hacker experience");
idea.setDescription("Better than all the rest");
idea.setProjectType(baseData.bachelor());
idea.setApplicationPeriod(applicationPeriod);
idea.setResearchArea(baseData.researchArea().researchArea());
ideaService.saveSupervisorIdea(idea, supervisor, baseData.researchArea().keywords(), true);
}
}

@ -0,0 +1,13 @@
/**
* Contains classes that populate the database with test data.
* <p>
* Prefer to use methods on the various services to create data, rather than directly interacting with the database
* using an {@link jakarta.persistence.EntityManager}. This is to make sure all business rules are enforced and that
* any additional logic is executed such as sending notifications or calculating statistics.
*
* @see se.su.dsv.scipro.testdata how to add new populators
* @see se.su.dsv.scipro.testdata.TestDataPopulator
* @see se.su.dsv.scipro.testdata.BaseData
* @see se.su.dsv.scipro.testdata.Factory
*/
package se.su.dsv.scipro.testdata.populators;

@ -0,0 +1 @@
se.su.dsv.scipro.testdata.TestDataConfiguration

@ -123,13 +123,7 @@ public class AdminApplicationPeriodsPanel extends Panel {
item.add(
new DisplayMultiplesPanel<>(
s,
new ListAdapterModel<>(
LambdaModel.of(
iModel,
ApplicationPeriod::getProjectTypes,
ApplicationPeriod::setProjectTypes
)
)
new ListAdapterModel<>(iModel.map(ApplicationPeriod::getProjectTypes))
) {
@Override
public Component getComponent(String componentId, IModel<ProjectType> t) {

@ -1,6 +1,5 @@
package se.su.dsv.scipro.datatables.project;
import com.google.common.eventbus.EventBus;
import jakarta.inject.Inject;
import java.util.*;
import org.apache.wicket.ajax.AjaxRequestTarget;
@ -33,7 +32,6 @@ import se.su.dsv.scipro.components.datatables.MultipleUsersColumn;
import se.su.dsv.scipro.components.datatables.UserColumn;
import se.su.dsv.scipro.dataproviders.FilteredDataProvider;
import se.su.dsv.scipro.datatables.AjaxCheckboxWrapper;
import se.su.dsv.scipro.notifications.NotificationController;
import se.su.dsv.scipro.profile.UserLinkPanel;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.project.ProjectService;
@ -45,7 +43,6 @@ import se.su.dsv.scipro.system.ProjectType;
import se.su.dsv.scipro.system.ProjectTypeService;
import se.su.dsv.scipro.system.ResearchArea;
import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.system.UserService;
import se.su.dsv.scipro.util.PageParameterKeys;
public class ProjectDataPanel extends Panel {
@ -170,6 +167,11 @@ public class ProjectDataPanel extends Panel {
) {
cellItem.add(new ReviewerColumnCell(componentId, rowModel));
}
@Override
public IModel<?> getDataModel(IModel<Project> rowModel) {
return rowModel.map(Project::getReviewer).map(User::getFullName);
}
};
}

@ -22,12 +22,7 @@ public class AddTargetLinkPanel extends Panel {
public AddTargetLinkPanel(String id, final IModel<ApplicationPeriod> model) {
super(id, model);
add(
new ListView<>(
"list",
new ListAdapterModel<>(
LambdaModel.of(model, ApplicationPeriod::getProjectTypes, ApplicationPeriod::setProjectTypes)
)
) {
new ListView<>("list", new ListAdapterModel<>(model.map(ApplicationPeriod::getProjectTypes))) {
@Override
protected void populateItem(ListItem<ProjectType> item) {
item.add(new Label("pc", item.getModelObject().getName()));

@ -42,6 +42,13 @@
</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="col-lg-offset-4 col-lg-4">
<div class="form-check">

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

@ -6,6 +6,7 @@ import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LambdaModel;
import se.su.dsv.scipro.components.DateLabel;
import se.su.dsv.scipro.data.enums.DateStyle;
import se.su.dsv.scipro.grading.ReportPdfResource;
import se.su.dsv.scipro.report.Report;
@ -32,7 +33,11 @@ public class DownloadPdfReportPanel extends Panel {
add(resourceLink);
resourceLink.add(new Label(PDF_LABEL, reportPdfResource.getFileName()));
resourceLink.add(
new DateLabel(PDF_UPLOAD_DATE, LambdaModel.of(report, Report::getLastModified, Report::setLastModified))
new DateLabel(
PDF_UPLOAD_DATE,
LambdaModel.of(report, Report::getLastModified, Report::setLastModified),
DateStyle.DATETIME
)
);
}
}

@ -6,29 +6,36 @@
<div class="col-lg-8">
<h4>Opposition report</h4>
<div class="row mb-4">
<div class="col-lg-8">
<div class="help-box">
Använd bedömningskriterierna i denna rapport och skriv dina synpunkter som opponent i fritextfälten
under varje kriterium. Du gör dock ingen poängbedömning men
är fri att skriva så mycket som du önskar på varje bedömningskriterium.
</div>
</div>
<div class="help-box mb-3">
Use the assessment criteria in this report and write your views as an opponent in the text
fields under each criterion. However, you do not make a point assessment but are free to
write as much as you wish on each criterion.
</div>
<div class="mb-3">
<strong>Final seminar file:</strong> <span wicket:id="thesisFile"></span>
</div>
<div wicket:id="fillOutReport">
<strong>Thesis summary</strong>
<wicket:enclosure child="improvements_requested_comment">
<div class="alert alert-info">
<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>
<label>
<textarea class="form-control mb-4" rows="8" wicket:id="thesisSummary"></textarea>
<p class="mb-0" wicket:id="improvements_requested_comment"></p>
</div>
</wicket:enclosure>
<div wicket:id="fillOutReport">
<label wicket:for="thesisSummary">
Thesis summary
</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>

@ -1,8 +1,11 @@
package se.su.dsv.scipro.finalseminar;
import jakarta.inject.Inject;
import java.time.ZoneId;
import java.util.Optional;
import org.apache.wicket.RestartResponseException;
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.model.IModel;
import org.apache.wicket.model.LambdaModel;
@ -24,7 +27,7 @@ public class OppositionReportPage extends AbstractProjectDetailsPage implements
public static final String FILL_OUT_REPORT = "fillOutReport";
@Inject
private FinalSeminarOppositionRepo finalSeminarOppositionRepo;
private FinalSeminarOppositionService finalSeminarOppositionService;
@Inject
private OppositionReportService oppositionReportService;
@ -35,13 +38,15 @@ public class OppositionReportPage extends AbstractProjectDetailsPage implements
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);
}
final IModel<OppositionReport> report = getOppositionReport(opposition);
final IModel<OppositionReport> report = opposition.map(Opposition::report);
add(
new ViewAttachmentPanel(
@ -50,8 +55,35 @@ public class OppositionReportPage extends AbstractProjectDetailsPage implements
)
);
IModel<Opposition.ImprovementsNeeded> improvements = opposition
.map(Opposition::improvementsNeeded)
.map(OppositionReportPage::orNull);
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<>(
THESIS_SUMMARY,
@ -71,18 +103,13 @@ public class OppositionReportPage extends AbstractProjectDetailsPage implements
@Override
protected void 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) {
return new LoadableDetachableModel<>() {
@Override
protected OppositionReport load() {
return oppositionReportService.findOrCreateReport(opposition);
}
};
private static <A> A orNull(Optional<A> optional) {
return optional.orElse(null);
}
}

@ -11,7 +11,7 @@
<div wicket:id="container">
<div wicket:id="opponents">
<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>
<div wicket:id="report"></div>
</div>
@ -19,13 +19,31 @@
<div class="col-lg-5">
<form wicket:id="form">
<div class="card mb-3 bg-info text-white">
<wicket:message key="criteria"/>
<div class="card mb-3 text-bg-info">
<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>
<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">
<label>Points:</label>
<input type="text" class="form-control gradingPoints" wicket:id="points"/>
<select class="form-select" wicket:id="points"></select>
</div>
<label>Motivation:</label>
@ -34,6 +52,25 @@
<button wicket:id="submit" type="submit" class="btn btn-sm btn-success">
<wicket:message key="submit"/>
</button>
<a class="btn btn-outline-secondary btn-sm" wicket:id="request_improvements">
Request improvements
</a>
</form>
<form wicket:id="request_improvements">
<p>
Once you request improvements the student have a limited time to make the requested changes.
If they do not make the requested improvements in time, they will get an automatic failing grade.
You will be notified when they've submitted a new report.
</p>
<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>
<div wicket:id="gradeContainer">

@ -1,19 +1,23 @@
package se.su.dsv.scipro.finalseminar;
import com.google.common.eventbus.EventBus;
import jakarta.inject.Inject;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.List;
import java.util.Objects;
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.feedback.FencedFeedbackPanel;
import org.apache.wicket.markup.html.WebMarkupContainer;
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.FormComponent;
import org.apache.wicket.markup.html.form.LambdaChoiceRenderer;
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.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
@ -22,14 +26,14 @@ import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;
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.validation.validator.RangeValidator;
import org.apache.wicket.validation.validator.StringValidator;
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.report.GradingReportService;
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.session.SciProSession;
import se.su.dsv.scipro.system.ProjectModule;
@ -43,8 +47,6 @@ public class SeminarOppositionPanel extends Panel {
public static final String REMOVE = "remove";
public static final String FORM = "form";
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 int FEEDBACK_MAX_LENGTH = 2000;
public static final String SUBMIT = "submit";
@ -55,18 +57,12 @@ public class SeminarOppositionPanel extends Panel {
@Inject
private FinalSeminarOppositionService finalSeminarOppositionService;
@Inject
private EventBus eventBus;
@Inject
private ProjectTypeService projectTypeService;
@Inject
private FinalSeminarService finalSeminarService;
@Inject
private GradingReportService gradingReportService;
@Inject
private OppositionReportService oppositionReportService;
@ -75,6 +71,9 @@ public class SeminarOppositionPanel extends Panel {
private final WebMarkupContainer oppositionContainer;
private final ListView<FinalSeminarOpposition> opponents;
private FinalSeminarOppositionForm gradeForm;
private RequestImprovementsForm requestImprovementsForm;
public SeminarOppositionPanel(String id, final IModel<FinalSeminar> seminar) {
super(id, seminar);
this.seminar = seminar;
@ -107,6 +106,12 @@ public class SeminarOppositionPanel extends Panel {
private ListView<FinalSeminarOpposition> getOpponentsList(final IModel<List<FinalSeminarOpposition>> 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
protected void populateItem(final ListItem<FinalSeminarOpposition> item) {
final FinalSeminarOpposition opposition = item.getModelObject();
@ -121,7 +126,14 @@ public class SeminarOppositionPanel extends Panel {
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()) {
item.add(new SeminarOppositionReportPanel("report", item.getModel()));
@ -211,29 +223,47 @@ public class SeminarOppositionPanel extends Panel {
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) {
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,
LambdaModel.of(
finalSeminarOpposition,
FinalSeminarOpposition::getPoints,
FinalSeminarOpposition::setPoints
)
)
.add(RangeValidator.range(MIN_POINTS, MAX_POINTS))
.setType(Integer.class)
.setRequired(true);
pointsModel,
criteriaModel.map(OppositionCriteria::pointsAvailable),
new LambdaChoiceRenderer<>(OppositionCriteria.Point::value)
);
pointsField.setRequired(true);
add(pointsField);
TextArea<String> feedback = new TextArea<>(
GRADING_FEEDBACK,
LambdaModel.of(
finalSeminarOpposition,
FinalSeminarOpposition::getFeedback,
FinalSeminarOpposition::setFeedback
)
);
TextArea<String> feedback = new TextArea<>(GRADING_FEEDBACK, feedbackModel);
feedback.add(StringValidator.maximumLength(FEEDBACK_MAX_LENGTH));
feedback.setRequired(true);
add(feedback);
@ -242,33 +272,19 @@ public class SeminarOppositionPanel extends Panel {
new AjaxSubmitLink(SUBMIT) {
@Override
protected void onSubmit(AjaxRequestTarget target) {
if (getModelObject().getPoints().equals(0)) {
finalSeminarOpposition.getObject().setGrade(FinalSeminarGrade.NOT_APPROVED);
eventBus.post(new OppositionFailedEvent(finalSeminarOpposition.getObject()));
} else {
finalSeminarOpposition.getObject().setGrade(FinalSeminarGrade.APPROVED);
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()
try {
finalSeminarOppositionService.gradeOpponent(
finalSeminarOpposition.getObject(),
pointsModel.getObject().value(),
feedbackModel.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
@ -277,17 +293,47 @@ 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
protected void onConfigure() {
super.onConfigure();
FinalSeminarOpposition opposition = getModelObject();
setVisibilityAllowed(
startDateHasPassed() &&
getModelObject().getPoints() == null &&
getModelObject().getFeedback() == null &&
opposition.getPoints() == null &&
opposition.getFeedback() == null &&
isHeadSupervisor()
);
boolean hasRequestedImprovements = opposition.getImprovementsRequestedAt() != null;
boolean reportIsSubmitted =
opposition.getOppositionReport() != null && opposition.getOppositionReport().isSubmitted();
setEnabled(!hasRequestedImprovements || reportIsSubmitted);
}
}
@ -298,4 +344,47 @@ public class SeminarOppositionPanel extends Panel {
private boolean hasSubmittedOppositionReport(FinalSeminarOpposition opposition) {
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);
}
}
}

@ -8,16 +8,15 @@ gradingFeedback.Required = You need to write a motivation
points.Required = Points are required
opponents.form.points.RangeValidator.range= Points assigned must be between ${minimum} and ${maximum}
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.
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.
criteria= As the supervisor on this final seminar you are also required to grade the opponents opposition report.
opposition.report= Opposition report:
removed= Opponent ${user.fullName} successfully removed
opposition.report.removed= Opposition report successfully removed
are.you.sure= Are you sure you want to remove this opponent report?
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.

@ -7,9 +7,13 @@
<div wicket:id="wmc">
<wicket:enclosure child="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>
<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">
Report attachment: <span wicket:id="downloadAttachment"></span>
</wicket:enclosure>

@ -70,6 +70,19 @@ public class SeminarOppositionReportPanel extends GenericPanel<FinalSeminarOppos
wmc.add(getDeleteOpponentReportLink(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) {

@ -7,9 +7,10 @@
<body>
<wicket:panel>
<div class="messageWrap">
<div class="forumBlueBackground">
<div class="forumBlueBackground d-flex justify-content-between">
<!-- DATE ROW-->
<wicket:container wicket:id="dateCreated"/>
<span wicket:id="dateCreated"></span>
<button wicket:id="delete" class="btn btn-sm btn-outline-danger ms-auto">Delete</button>
</div>
<div class="forumGrayBackground">
<div class="vertAlign">

@ -1,7 +1,9 @@
package se.su.dsv.scipro.forum.panels.threaded;
import jakarta.inject.Inject;
import org.apache.wicket.Component;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LambdaModel;
@ -11,9 +13,11 @@ import se.su.dsv.scipro.components.ListAdapterModel;
import se.su.dsv.scipro.components.SmarterLinkMultiLineLabel;
import se.su.dsv.scipro.data.enums.DateStyle;
import se.su.dsv.scipro.file.FileReference;
import se.su.dsv.scipro.forum.BasicForumService;
import se.su.dsv.scipro.forum.dataobjects.ForumPost;
import se.su.dsv.scipro.profile.UserLinkPanel;
import se.su.dsv.scipro.repository.panels.ViewAttachmentPanel;
import se.su.dsv.scipro.session.SciProSession;
public class ForumPostPanel extends Panel {
@ -22,6 +26,9 @@ public class ForumPostPanel extends Panel {
public static final String CONTENT = "content";
public static final String ATTACHMENT = "attachment";
@Inject
private BasicForumService basicForumService;
public ForumPostPanel(String id, final IModel<ForumPost> model) {
super(id);
add(new UserLinkPanel(POSTED_BY, LambdaModel.of(model, ForumPost::getPostedBy, ForumPost::setPostedBy)));
@ -62,5 +69,28 @@ public class ForumPostPanel extends Panel {
}
}
);
add(
new Link<>("delete", model) {
@Override
public void onClick() {
ForumPost post = getModelObject();
basicForumService.deletePost(post);
onPostDeleted();
}
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(allowDeletion() && basicForumService.canDelete(getModelObject()));
}
}
);
}
protected boolean allowDeletion() {
return false;
}
protected void onPostDeleted() {}
}

@ -1,8 +1,7 @@
package se.su.dsv.scipro.forum.panels.threaded;
import jakarta.inject.Inject;
import java.io.Serializable;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
@ -15,6 +14,7 @@ import org.apache.wicket.model.LambdaModel;
import org.apache.wicket.model.LoadableDetachableModel;
import se.su.dsv.scipro.components.DateLabel;
import se.su.dsv.scipro.data.enums.DateStyle;
import se.su.dsv.scipro.forum.BasicForumService;
import se.su.dsv.scipro.forum.Discussable;
import se.su.dsv.scipro.forum.dataobjects.ForumPost;
import se.su.dsv.scipro.forum.dataobjects.ForumThread;
@ -23,6 +23,9 @@ import se.su.dsv.scipro.system.User;
public class ThreadsOverviewPanel<A> extends Panel {
@Inject
private BasicForumService basicForumService;
public ThreadsOverviewPanel(
final String id,
final IModel<List<A>> model,
@ -41,7 +44,7 @@ public class ThreadsOverviewPanel<A> extends Panel {
@Override
protected void onConfigure() {
super.onConfigure();
setVisibilityAllowed(discussion.getObject().hasAttachments());
setVisibilityAllowed(basicForumService.hasAttachments(discussion.getObject()));
}
}
);
@ -80,7 +83,7 @@ public class ThreadsOverviewPanel<A> extends Panel {
BookmarkablePageLink<Void> newThreadLink(String id, IModel<A> thread);
}
private static class LastPostColumn extends WebMarkupContainer {
private class LastPostColumn extends WebMarkupContainer {
public LastPostColumn(String id, final IModel<ForumThread> model) {
super(id);
@ -110,10 +113,7 @@ public class ThreadsOverviewPanel<A> extends Panel {
return new LoadableDetachableModel<>() {
@Override
protected ForumPost load() {
return Collections.max(
model.getObject().getPosts(),
Comparator.comparing(ForumPost::getDateCreated).thenComparing(ForumPost::getId)
);
return basicForumService.getLastPost(model.getObject());
}
};
}

@ -58,7 +58,21 @@ public class ViewForumThreadPanel<A> extends GenericPanel<A> {
new ListView<>(POST_LIST, new PostProvider()) {
@Override
protected void populateItem(ListItem<ForumPost> item) {
item.add(new ForumPostPanel(POST, item.getModel()));
ListView<ForumPost> listView = this;
item.add(
new ForumPostPanel(POST, item.getModel()) {
@Override
protected boolean allowDeletion() {
return true;
}
@Override
protected void onPostDeleted() {
// Refresh the list of posts
listView.detach();
}
}
);
}
}
);

@ -5,30 +5,26 @@
</head>
<body>
<wicket:border>
<div class="row">
<div class="col-lg-8">
<div wicket:id="save"></div>
<form wicket:id="form">
<div wicket:id="feedbackPanel"></div>
<wicket:body/>
<div wicket:id="criteria">
<strong><span wicket:id="title"></span></strong>
<div wicket:id="save"></div>
<form wicket:id="form">
<div wicket:id="feedbackPanel"></div>
<wicket:body/>
<div wicket:id="criteria">
<strong><span wicket:id="title"></span></strong>
<p><span wicket:id="description" class="gradingCriteria"></span></p>
<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>
<p><span wicket:id="description" class="gradingCriteria"></span></p>
<textarea class="form-control mb-4" rows="8" cols="5" wicket:id="feedback"></textarea>
</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>
</body>
</html>

@ -25,12 +25,12 @@ import se.su.dsv.scipro.files.WicketFileUpload;
import se.su.dsv.scipro.report.AttachmentReport;
import se.su.dsv.scipro.report.Criterion;
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.system.Language;
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 GRADING_CRITERIA = "criteria";
@ -42,20 +42,20 @@ public class FillOutReportPanel<T extends OppositionReport> extends Border {
public static final String FEEDBACK_PANEL = "feedbackPanel";
@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);
ReportForm form = new ReportForm(FORM, model);
addToBorder(new ScrollingSaveButtonPanel(SAVE, form));
addToBorder(form);
}
private class ReportForm extends StatelessForm<T> {
private class ReportForm extends StatelessForm<OppositionReport> {
private final FileUploadField attachment;
public ReportForm(String id, final IModel<T> model) {
public ReportForm(String id, final IModel<OppositionReport> model) {
super(id, model);
add(new ComponentFeedbackPanel(FEEDBACK_PANEL, this));
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);
add(new JavascriptEventConfirmation("click", new ResourceModel("delete.attachment")));
}

@ -2,85 +2,50 @@
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
<body>
<wicket:panel>
<div class="row">
<div class="col-lg-12">
<form wicket:id="form">
<div class="line-length-limit">
<form wicket:id="form">
<div class="row">
<div class="col-lg-12">
<div wicket:id="feedback"></div>
</div>
</div>
<div wicket:id="feedback"></div>
<div class="row">
<div class="col-lg-5 col-md-10">
<label wicket:for="title">Title: </label>
<input type="text" wicket:id="title" class="form-control">
</div>
</div>
<div class="mb-3">
<label wicket:for="title" class="form-label">Title</label>
<input type="text" wicket:id="title" class="form-control">
</div>
<div class="row">
<div class="col-lg-5 col-md-10">
<label wicket:for="description">Description: </label>
<textarea wicket:id="description" class="form-control"></textarea>
</div>
</div>
<div class="mb-3">
<label wicket:for="description" class="form-label">Description</label>
<textarea wicket:id="description" class="form-control"></textarea>
</div>
<div class="row">
<div class="col-lg-5 col-md-10">
<div class="form-check">
<input class="form-check-input" wicket:id="active" type="checkbox"/>
<label class="form-check-label" wicket:for="active">Active</label>
<div class="form-check mb-3">
<input class="form-check-input" wicket:id="active" type="checkbox"/>
<label class="form-check-label" wicket:for="active">Active</label>
</div>
<fieldset class="mb-3">
<legend>Projects</legend>
<div class="group-project-grid">
<label wicket:id="available_projects">
<div>
<input class="form-check-input mt-0" type="checkbox" wicket:id="selected">
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div wicket:id="wmc">
<div class="row">
<div class="col-lg-5 col-md-10">
<strong>Add projects to group: </strong>
<div wicket:id="projectTypes"></div>
<select class="form-select" wicket:id="addProjects"></select>
</div>
<div>
<h4 wicket:id="title"></h4>
<span wicket:id="type"></span>
<br>
Started at <span wicket:id="start_date"></span>
<div wicket:id="authors">
<span wicket:id="author"></span>
</div>
<div class="row">
<div class="col-lg-12">
<strong>Projects in group: </strong>
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Type</th>
<th>Title</th>
<th>Authors</th>
<th>Remove</th>
</tr>
</thead>
<tbody>
<tr wicket:id="projects">
<td><span wicket:id="type"></span></td>
<td><span wicket:id="title"></span></td>
<td><div wicket:id="authors">
<div wicket:id="author"></div>
</div></td>
<td><a wicket:id="remove"><span class="fa fa-times"></span></a></td>
</tr>
</tbody>
</table>
<div wicket:id="noProjects"></div>
</div>
</div>
</div>
</div>
</label>
</div>
<br>
<button type="submit" class="btn btn-success" >Save</button>
</form>
</div>
</fieldset>
<button type="submit" class="btn btn-success">Save</button>
<button type="submit" wicket:id="save_and_close" class="btn btn-success">Save and close</button>
<button type="submit" wicket:id="save_and_create" class="btn btn-success">Save and create another</button>
<a wicket:id="cancel" class="btn btn-outline-secondary">Cancel</a>
</form>
</div>
</wicket:panel>
</body>

@ -2,11 +2,10 @@ package se.su.dsv.scipro.group;
import jakarta.inject.Inject;
import java.util.*;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.extensions.model.AbstractCheckBoxModel;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.*;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
@ -14,10 +13,6 @@ import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LambdaModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.util.ListModel;
import se.su.dsv.scipro.components.AjaxCheckBoxMultipleChoice;
import se.su.dsv.scipro.components.AjaxDropDownChoice;
import se.su.dsv.scipro.components.ListAdapterModel;
import se.su.dsv.scipro.profile.UserLinkPanel;
import se.su.dsv.scipro.project.Project;
@ -25,8 +20,8 @@ import se.su.dsv.scipro.project.ProjectService;
import se.su.dsv.scipro.project.ProjectStatus;
import se.su.dsv.scipro.project.ProjectTeamMemberRoles;
import se.su.dsv.scipro.session.SciProSession;
import se.su.dsv.scipro.system.ProjectType;
import se.su.dsv.scipro.system.ProjectTypeService;
import se.su.dsv.scipro.supervisor.pages.SupervisorEditGroupPage;
import se.su.dsv.scipro.supervisor.pages.SupervisorMyGroupsPage;
import se.su.dsv.scipro.system.User;
public class EditGroupPanel extends Panel {
@ -37,9 +32,6 @@ public class EditGroupPanel extends Panel {
@Inject
private GroupService groupService;
@Inject
private ProjectTypeService projectTypeService;
public EditGroupPanel(String id, final IModel<Group> model) {
super(id, model);
add(new GroupForm("form", model));
@ -47,18 +39,51 @@ public class EditGroupPanel extends Panel {
private class GroupForm extends Form<Group> {
private final AjaxDropDownChoice<Project> addProjects;
private final ListView<Project> projects;
private final List<Project> currentProjects;
private final AjaxCheckBoxMultipleChoice<ProjectType> projectTypes;
public GroupForm(String form, final IModel<Group> model) {
super(form, model);
final FeedbackPanel feedbackPanel = new FeedbackPanel("feedback");
feedbackPanel.setOutputMarkupId(true);
add(feedbackPanel);
currentProjects = new ArrayList<>(getModelObject().getProjects());
IModel<List<Project>> availableProjects = LoadableDetachableModel.of(() -> {
Set<Project> projects = new HashSet<>();
projects.addAll(getAllRelevantProjects());
// Have to add the projects that are already in the group to the list of available projects
// since they may not be included in the relevant projects if they're inactive or completed.
// To allow them to be removed from the group, it will not be possible to add them again.
projects.addAll(model.getObject().getProjects());
return projects
.stream()
.sorted(Comparator.comparing(Project::getStartDate).reversed().thenComparing(Project::getTitle))
.toList();
});
add(
new ListView<>("available_projects", availableProjects) {
{
// must re-use list items to maintain form component (checkboxes) state
setReuseItems(true);
}
@Override
protected void populateItem(ListItem<Project> item) {
CheckBox checkbox = new CheckBox("selected", new SelectProjectModel(model, item.getModel()));
checkbox.setOutputMarkupId(true);
item.add(checkbox);
item.add(new Label("title", item.getModel().map(Project::getTitle)));
item.add(new Label("type", item.getModel().map(Project::getProjectTypeName)));
item.add(new Label("start_date", item.getModel().map(Project::getStartDate)));
IModel<SortedSet<User>> authors = item.getModel().map(Project::getProjectParticipants);
item.add(
new ListView<>("authors", new ListAdapterModel<>(authors)) {
@Override
protected void populateItem(ListItem<User> item) {
item.add(new UserLinkPanel("author", item.getModel()));
}
}
);
}
}
);
add(new RequiredTextField<>("title", LambdaModel.of(model, Group::getTitle, Group::setTitle)));
add(new TextArea<>("description", LambdaModel.of(model, Group::getDescription, Group::setDescription)));
@ -66,120 +91,64 @@ public class EditGroupPanel extends Panel {
new CheckBox("active", LambdaModel.of(model, Group::isActive, Group::setActive)).setOutputMarkupId(true)
);
final WebMarkupContainer wmc = new WebMarkupContainer("wmc");
wmc.setOutputMarkupId(true);
projectTypes = projectTypeSelection(wmc);
wmc.add(projectTypes);
addProjects = new AjaxDropDownChoice<>(
"addProjects",
new Model<>(),
getSelectableProjects(currentProjects),
new LambdaChoiceRenderer<>(Project::getTitle, Project::getId)
) {
@Override
public void onNewSelection(AjaxRequestTarget target, Project objectSelected) {
if (objectSelected != null && !currentProjects.contains(objectSelected)) {
currentProjects.add(objectSelected);
projects.setList(currentProjects);
addProjects.setChoices(getSelectableProjects(currentProjects));
target.add(wmc);
}
}
};
addProjects.setRequired(false);
addProjects.setNullValid(true);
wmc.add(addProjects);
projects = new ListView<>("projects", new ArrayList<>(currentProjects)) {
@Override
protected void populateItem(final ListItem<Project> item) {
item.add(new Label("type", item.getModel().map(Project::getProjectTypeName)));
item.add(new Label("title", item.getModel().map(Project::getTitle)));
item.add(
new ListView<>(
"authors",
new ListAdapterModel<>(
getLoaded(item.getModelObject()).map(Project::getProjectParticipants)
)
) {
@Override
public void populateItem(ListItem<User> item) {
item.add(new UserLinkPanel("author", item.getModel()));
}
}
);
item.add(
new AjaxLink<>("remove", item.getModel()) {
@Override
public void onClick(AjaxRequestTarget target) {
currentProjects.remove(item.getModelObject());
projects.setList(currentProjects);
addProjects.setChoices(getSelectableProjects(currentProjects));
target.add(wmc);
}
}
);
}
};
wmc.add(projects);
wmc.add(
new Label("noProjects", "None") {
add(
new SubmitLink("save_and_close") {
@Override
protected void onConfigure() {
super.onConfigure();
setVisibilityAllowed(currentProjects.isEmpty());
public void onAfterSubmit() {
setResponsePage(SupervisorMyGroupsPage.class);
}
}
);
add(wmc);
}
private AjaxCheckBoxMultipleChoice<ProjectType> projectTypeSelection(final WebMarkupContainer wmc) {
return new AjaxCheckBoxMultipleChoice<>(
"projectTypes",
projectTypeService.findAllActive(),
projectTypeService.findAllActive(),
new LambdaChoiceRenderer<>(ProjectType::getName, ProjectType::getId)
) {
@Override
public void onUpdate(AjaxRequestTarget target) {
addProjects.setChoices(getSelectableProjects(currentProjects));
target.add(wmc);
add(
new SubmitLink("save_and_create") {
@Override
public void onAfterSubmit() {
setResponsePage(SupervisorEditGroupPage.class);
}
}
};
);
add(new BookmarkablePageLink<>("cancel", SupervisorMyGroupsPage.class));
}
@Override
protected void onSubmit() {
Group group = getModelObject();
group.setProjects(new HashSet<>(currentProjects));
groupService.save(group);
info(getString("saved"));
}
private ListModel<Project> getSelectableProjects(List<Project> currentProjects) {
private List<Project> getAllRelevantProjects() {
final ProjectService.Filter filter = new ProjectService.Filter();
filter.setSupervisor(SciProSession.get().getUser());
filter.setRoles(Collections.singleton(ProjectTeamMemberRoles.CO_SUPERVISOR));
filter.setStatuses(Collections.singletonList(ProjectStatus.ACTIVE));
filter.setProjectTypes(projectTypes.getModelObject());
List<Project> all = projectService.findAll(filter);
all.removeAll(currentProjects);
all.remove(null);
return new ListModel<>(all);
return projectService.findAll(filter);
}
private LoadableDetachableModel<Project> getLoaded(final Project project) {
return new LoadableDetachableModel<>() {
@Override
protected Project load() {
return projectService.findOne(project.getId());
}
};
private static final class SelectProjectModel extends AbstractCheckBoxModel {
private final IModel<Group> groupModel;
private final IModel<Project> projectModel;
public SelectProjectModel(IModel<Group> groupModel, IModel<Project> projectModel) {
this.groupModel = groupModel;
this.projectModel = projectModel;
}
@Override
public boolean isSelected() {
return groupModel.getObject().getProjects().contains(projectModel.getObject());
}
@Override
public void select() {
groupModel.getObject().getProjects().add(projectModel.getObject());
}
@Override
public void unselect() {
groupModel.getObject().getProjects().remove(projectModel.getObject());
}
}
}
}

@ -72,7 +72,9 @@ public class ExcelExporter extends AbstractDataExporter {
for (int i = 0; i < columns.size(); i++) {
Object cellValue = columns.get(i).getDataModel(data).getObject();
Cell cell = row.createCell(i);
cell.setCellValue(String.valueOf(cellValue));
if (cellValue != null) {
cell.setCellValue(cellValue.toString());
}
}
}
}

@ -10,6 +10,8 @@ import org.apache.wicket.util.string.StringValueConversionException;
import se.su.dsv.scipro.activityplan.ProjectActivityPlanPage;
import se.su.dsv.scipro.activityplan.SupervisorActivityPlanPage;
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.ProjectFinalSeminarPage;
import se.su.dsv.scipro.finalseminar.ProjectOppositionPage;
@ -217,6 +219,19 @@ public class NotificationLandingPage extends WebPage {
} else if (
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);
}
}

@ -76,13 +76,7 @@ public class ProjectPartnerPage extends AbstractIdeaProjectPage implements MenuH
}
);
final IModel<? extends List<ProjectType>> matchableTypes = getMatchableTypes(
new ListAdapterModel<>(
LambdaModel.of(
applicationPeriod,
ApplicationPeriod::getProjectTypes,
ApplicationPeriod::setProjectTypes
)
)
new ListAdapterModel<>(applicationPeriod.map(ApplicationPeriod::getProjectTypes))
);
panelContainer.add(
new ListView<>("ads", matchableTypes) {

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

@ -607,3 +607,27 @@ th.wicket_orderUp, th.sorting_asc {
.line-length-limit {
max-width: 80em;
}
.group-project-grid {
display: flex;
flex-wrap: wrap;
gap: 1em;
grid-template-columns: repeat(auto-fill, minmax(30em, 1fr));
}
.group-project-grid > * {
background: linear-gradient(to left, white 40%, var(--bs-success-bg-subtle) 60%) right;
background-size: 250% 100%;
transition: background 0.4s ease;
cursor: pointer;
border: 1px solid black;
border-radius: 0.25em;
display: flex;
padding: 0.5em;
align-items: center;
flex-grow: 1;
}
.group-project-grid > *:has(:checked) {
background-position: left;
}
.group-project-grid label {
font-weight: normal;
}

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

@ -17,7 +17,6 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import se.su.dsv.scipro.SciProTest;
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.GradingReportTemplate;
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.ProjectType;
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 CRITERTION_TITLE = "U1 Sammanfattning";
@Mock
private ReportService reportService;
private FinalSeminarOpposition finalSeminarOpposition;
private User user;
private ProjectType bachelor;
@ -76,10 +71,9 @@ public class OppositionReportPageTest extends SciProTest {
Mockito.when(finalSeminarService.findByProject(opponentsProject)).thenReturn(finalSeminar);
}
private void mockReport(ProjectType bachelor) {
private OppositionReport mockReport(ProjectType bachelor) {
GradingReportTemplate reportTemplate = createTemplate(bachelor);
OppositionReport oppositionReport = reportTemplate.createOppositionReport(finalSeminarOpposition);
Mockito.when(oppositionReportService.findOrCreateReport(finalSeminarOpposition)).thenReturn(oppositionReport);
return reportTemplate.createOppositionReport(finalSeminarOpposition);
}
@Test
@ -104,14 +98,16 @@ public class OppositionReportPageTest extends SciProTest {
public void disable_form_if_opposition_does_not_belong_to_logged_in_user() {
mockReport(bachelor);
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);
tester.assertDisabled(FILL_OUT_REPORT);
}
@Test
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);
tester.assertRenderedPage(ProjectDetailsPage.class);
}
@ -138,7 +134,7 @@ public class OppositionReportPageTest extends SciProTest {
formTester.submit();
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());
}
@ -156,7 +152,9 @@ public class OppositionReportPageTest extends SciProTest {
private void startOppositionPage() {
long oppositionId = 4L;
setLoggedInAs(user);
Mockito.when(finalSeminarOppositionRepo.findOne(oppositionId)).thenReturn(finalSeminarOpposition);
Mockito.when(finalSeminarOppositionService.getOpposition(oppositionId)).thenReturn(
new Opposition(user, mockReport(bachelor), Optional.empty())
);
startPage(oppositionId);
}

@ -61,6 +61,15 @@ public class SeminarOppositionPanelTest extends SciProTest {
finalSeminar.setProject(project);
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

@ -66,6 +66,14 @@ public class SeminarPanelTest extends SciProTest {
Mockito.when(plagiarismControl.getStatus(any(FileDescription.class))).thenReturn(
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() {

@ -9,6 +9,7 @@ import org.apache.wicket.model.Model;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import se.su.dsv.scipro.SciProTest;
import se.su.dsv.scipro.forum.Discussable;
@ -20,11 +21,13 @@ import se.su.dsv.scipro.system.User;
public class ThreadsOverviewPanelTest extends SciProTest {
private List<ForumThread> threads;
private ForumPost post;
@BeforeEach
public void setUp() throws Exception {
ForumThread forumThread = createThread();
threads = Arrays.asList(forumThread);
Mockito.when(basicForumService.getLastPost(forumThread)).thenReturn(post);
}
@Test
@ -54,7 +57,7 @@ public class ThreadsOverviewPanelTest extends SciProTest {
private ForumThread createThread() {
User bob = User.builder().firstName("Bob").lastName("the Builder").emailAddress("bob@building.com").build();
ForumPost post = new ForumPost();
post = new ForumPost();
post.setPostedBy(bob);
ForumThread groupForumThread = new ForumThread();
groupForumThread.addPost(post);

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

@ -27,7 +27,6 @@ public class EditGroupPanelTest extends SciProTest {
group.setId(1L);
Project project = createProject();
group.setProjects(new HashSet<>(Collections.singletonList(project)));
when(projectService.findOne(anyLong())).thenReturn(project);
startPanel();
}

@ -140,5 +140,18 @@
<spring.profile.active>branch</spring.profile.active>
</properties>
</profile>
<profile>
<id>DEV</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<dependencies>
<dependency>
<groupId>se.su.dsv.scipro</groupId>
<artifactId>test-data</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</profile>
</profiles>
</project>

@ -4,6 +4,7 @@ import jakarta.inject.Inject;
import jakarta.inject.Provider;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.security.Principal;
import java.util.Collections;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@ -81,7 +82,7 @@ public class CurrentUserFromSpringSecurity implements AuthenticationContext {
return authentication.getName();
}
private static final class WicketControlledPrincipal implements Principal {
private static final class WicketControlledPrincipal implements Principal, Serializable {
private final String username;

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