3122 Stöd för manuell hantering av slutsem för handledare

This commit is contained in:
Wayne Westmoreland 2023-11-23 12:55:10 +01:00
parent 6b6c535e97
commit 9628c6a28f
17 changed files with 383 additions and 195 deletions

@ -0,0 +1,76 @@
package se.su.dsv.scipro.finalseminar;
import java.util.function.Function;
public abstract class ActiveParticipationRegistrationStatus {
ActiveParticipationRegistrationStatus() {
}
public abstract <A> A fold(
Function<TooManyParticipants, A> a,
Function<ManualParticipants, A> b,
Function<ParticipationAlreadyParticipating, A> e,
Function<ParticipationAlreadyHappened, A> f,
Function<ParticipationFinalSeminarCancelled, A> g);
}
final class TooManyParticipants extends ActiveParticipationRegistrationStatus {
@Override
public <A> A fold(
Function<TooManyParticipants, A> a,
Function<ManualParticipants, A> b,
Function<ParticipationAlreadyParticipating, A> e,
Function<ParticipationAlreadyHappened, A> f,
Function<ParticipationFinalSeminarCancelled, A> g) {
return a.apply(this);
}
}
final class ManualParticipants extends ActiveParticipationRegistrationStatus {
@Override
public <A> A fold(
Function<TooManyParticipants, A> a,
Function<ManualParticipants, A> b,
Function<ParticipationAlreadyParticipating, A> e,
Function<ParticipationAlreadyHappened, A> f,
Function<ParticipationFinalSeminarCancelled, A> g) {
return b.apply(this);
}
}
final class ParticipationAlreadyParticipating extends ActiveParticipationRegistrationStatus {
@Override
public <A> A fold(
Function<TooManyParticipants, A> a,
Function<ManualParticipants, A> b,
Function<ParticipationAlreadyParticipating, A> e,
Function<ParticipationAlreadyHappened, A> f,
Function<ParticipationFinalSeminarCancelled, A> g) {
return e.apply(this);
}
}
final class ParticipationAlreadyHappened extends ActiveParticipationRegistrationStatus {
@Override
public <A> A fold(
Function<TooManyParticipants, A> a,
Function<ManualParticipants, A> b,
Function<ParticipationAlreadyParticipating, A> e,
Function<ParticipationAlreadyHappened, A> f,
Function<ParticipationFinalSeminarCancelled, A> g) {
return f.apply(this);
}
}
final class ParticipationFinalSeminarCancelled extends ActiveParticipationRegistrationStatus {
@Override
public <A> A fold(
Function<TooManyParticipants, A> a,
Function<ManualParticipants, A> b,
Function<ParticipationAlreadyParticipating, A> e,
Function<ParticipationAlreadyHappened, A> f,
Function<ParticipationFinalSeminarCancelled, A> g) {
return g.apply(this);
}
}

@ -11,5 +11,5 @@ import java.util.*;
@Transactional
public interface FinalSeminarOppositionRepo extends JpaRepository<FinalSeminarOpposition, Long>, QueryDslPredicateExecutor<FinalSeminarOpposition> {
List<FinalSeminarOpposition> findByOpposingUserAndLevel(User user, ProjectType projectType);
List<FinalSeminarOpposition> findByOpposingUserAndType(User user, ProjectType projectType);
}

@ -16,7 +16,7 @@ public class FinalSeminarOppositionRepoImpl extends GenericRepo<FinalSeminarOppo
}
@Override
public List<FinalSeminarOpposition> findByOpposingUserAndLevel(User user, ProjectType projectType) {
public List<FinalSeminarOpposition> findByOpposingUserAndType(User user, ProjectType projectType) {
return createQuery()
.where(QFinalSeminarOpposition.finalSeminarOpposition.user.eq(user))
.where(QFinalSeminarOpposition.finalSeminarOpposition.project.projectType.eq(projectType))

@ -15,28 +15,37 @@ import java.util.List;
import java.util.Objects;
public interface FinalSeminarService extends GenericService<FinalSeminar, Long>, FilteredService<FinalSeminar, Long, FinalSeminarService.Filter>, FinalSeminarScheduling {
se.su.dsv.scipro.util.Either<OppositionRegistrationStatus, FinalSeminarOpposition> studentOppose(User student, FinalSeminar finalSeminar, Project project);
Either<OpposeError, FinalSeminarOpposition> oppose(User student, FinalSeminar finalSeminar, Project project);
void participate(User student, FinalSeminar finalSeminar, Project project);
se.su.dsv.scipro.util.Either<OppositionRegistrationStatus, Void> canOppose(User user, FinalSeminar finalSeminar, Project project);
boolean canParticipate(User user, FinalSeminar finalSeminar);
Either<OppositionRegistrationErrorStatus, FinalSeminarOpposition> attemptAddOpposition(User student, FinalSeminar finalSeminar, Project project);
Either<ActiveParticipationRegistrationStatus, FinalSeminarActiveParticipation> attemptAddActiveParticipation(User student, FinalSeminar finalSeminar, Project project);
Either<OpposeError, FinalSeminarOpposition> SupervisorAttemptAddOpposition(User student, FinalSeminar finalSeminar, Project project);
Either<ParticipateError, FinalSeminarActiveParticipation> SupervisorAttemptAddActiveParticipation(User student, FinalSeminar finalSeminar, Project project);
Either<OppositionRegistrationErrorStatus, Void> canOppose(User Student, FinalSeminar finalSeminar, Project project);
Either<ActiveParticipationRegistrationStatus, Void> canActiveParticipate(User student, FinalSeminar finalSeminar);
Iterable<FinalSeminar> findAll(Filter params);
Date thesisUploadDeadline(FinalSeminar finalSeminar);
boolean hasDeadlinePassed(FinalSeminar finalSeminar);
boolean hasThesis(FinalSeminar finalSeminar);
FinalSeminar findByProject(Project project);
Iterable<FinalSeminar> findByStartDateAfter(Date date);
boolean hasThesis(FinalSeminar finalSeminar);
FinalSeminar findByProject(Project project);
Iterable<FinalSeminar> findByStartDateAfter(Date date);
List<FinalSeminar> findProjectOpposing(Project project);
FinalSeminar deleteThesis(FinalSeminar seminar);
@Override
void delete(FinalSeminar finalSeminar);
boolean isActiveParticipant(FinalSeminar seminar, User activeParticipant);
boolean hasHadFinalSeminar(Project project);
List<FinalSeminar> findUnfinishedSeminars(Date after, Date before, Pageable pageable);

@ -23,11 +23,12 @@ import se.su.dsv.scipro.util.Either;
import javax.inject.Inject;
import javax.inject.Provider;
import java.time.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import static com.querydsl.core.types.dsl.Expressions.allOf;
import static com.querydsl.core.types.dsl.Expressions.*;
public class FinalSeminarServiceImpl extends AbstractServiceImpl<FinalSeminar, Long> implements FinalSeminarService {
@ -178,16 +179,6 @@ public class FinalSeminarServiceImpl extends AbstractServiceImpl<FinalSeminar, L
super.delete(seminar);
}
@Override
public void participate(User student, FinalSeminar finalSeminar, Project project) {
FinalSeminarActiveParticipation participation = new FinalSeminarActiveParticipation();
participation.setUser(student);
participation.setFinalSeminar(finalSeminar);
participation.setProject(project);
finalSeminar.addActiveParticipant(participation);
finalSeminarRepository.save(finalSeminar);
}
private boolean alreadyOpponent(User user, FinalSeminar finalSeminar) {
for (FinalSeminarOpposition fso : finalSeminar.getOppositions()) {
if (fso.getUser().equals(user)) {
@ -197,7 +188,7 @@ public class FinalSeminarServiceImpl extends AbstractServiceImpl<FinalSeminar, L
return false;
}
private boolean alreadyParticipant(User user, FinalSeminar finalSeminar) {
private boolean alreadyActiveParticipant(User user, FinalSeminar finalSeminar) {
for (FinalSeminarActiveParticipation fsap : finalSeminar.getActiveParticipations()) {
if (fsap.getUser().equals(user)) {
return true;
@ -216,80 +207,122 @@ public class FinalSeminarServiceImpl extends AbstractServiceImpl<FinalSeminar, L
}
@Override
public se.su.dsv.scipro.util.Either<OppositionRegistrationStatus, Void> canOppose(User user, FinalSeminar finalSeminar, Project project) {
public Either<OppositionRegistrationErrorStatus, Void> canOppose(User Student, FinalSeminar finalSeminar, Project project) {
if (finalSeminar.isCancelled()) {
return se.su.dsv.scipro.util.Either.left(new FinalSeminarCancelled());
return Either.left(new FinalSeminarCancelled());
}
if (!notAlreadyInSeminar(user, finalSeminar)) {
return se.su.dsv.scipro.util.Either.left(new AlreadyParticipating());
if (alreadyParticipatingInSeminar(Student, finalSeminar)) {
return Either.left(new AlreadyParticipating());
}
if (finalSeminar.getStartDate().before(new Date())) {
return se.su.dsv.scipro.util.Either.left(new AlreadyHappened());
return Either.left(new AlreadyHappened());
}
if (finalSeminar.getManualParticipants()) {
return Either.left(new ManualOpponents());
}
if (finalSeminar.getOppositions().size() >= finalSeminar.getMaxOpponents()) {
return se.su.dsv.scipro.util.Either.left(new TooManyOpponents());
return Either.left(new TooManyOpponents());
}
for (FinalSeminarOpposition opposition : finalSeminarOppositionRepository.findByOpposingUserAndLevel(user, project.getProjectType())) {
for (FinalSeminarOpposition opposition : finalSeminarOppositionRepository.findByOpposingUserAndType(Student, project.getProjectType())) {
if (opposition.getGrade() == null) {
return se.su.dsv.scipro.util.Either.left(new UngradedOpposition());
}
else if (opposition.getGrade() == FinalSeminarGrade.APPROVED) {
return se.su.dsv.scipro.util.Either.left(new AlreadyOpposed());
return Either.left(new UngradedOpposition());
} else if (opposition.getGrade() == FinalSeminarGrade.APPROVED) {
return Either.left(new AlreadyOpposed());
}
}
int oppositionPriorityDays = finalSeminarSettingsService.getInstance().getOppositionPriorityDays();
final Instant seminarDate = finalSeminar.getDateCreated().toInstant();
final Instant available = seminarDate.plus(Period.ofDays(oppositionPriorityDays));
if (available.isAfter(Instant.now()) && findByProject(project) == null) {
return se.su.dsv.scipro.util.Either.left(new PriorityForSeminarAuthors(oppositionPriorityDays, available));
return Either.left(new PriorityForSeminarAuthors(oppositionPriorityDays, available));
}
return se.su.dsv.scipro.util.Either.right(null);
return Either.right(null);
}
private boolean notAlreadyInSeminar(User user, FinalSeminar finalSeminar) {
return !alreadyOpponent(user, finalSeminar) && !isAuthor(user, finalSeminar) && !alreadyParticipant(user, finalSeminar);
@Override
public Either<ActiveParticipationRegistrationStatus, Void> canActiveParticipate(User student, FinalSeminar finalSeminar) {
if (finalSeminar.getManualParticipants()) {
return Either.left(new ManualParticipants());
}
if (alreadyParticipatingInSeminar(student, finalSeminar)) {
return Either.left(new ParticipationAlreadyParticipating());
}
if (finalSeminar.getActiveParticipations().size() >= finalSeminar.getMaxParticipants()) {
return Either.left(new TooManyParticipants());
}
if (finalSeminar.getStartDate().before(new Date())) {
return Either.left(new ParticipationAlreadyHappened());
}
if (finalSeminar.isCancelled()) {
return Either.left(new ParticipationFinalSeminarCancelled());
}
return Either.right(null);
}
private boolean alreadyParticipatingInSeminar(User user, FinalSeminar finalSeminar) {
return alreadyOpponent(user, finalSeminar) || isAuthor(user, finalSeminar) || alreadyActiveParticipant(user, finalSeminar);
}
@Override
@Transactional
public se.su.dsv.scipro.util.Either<OppositionRegistrationStatus, FinalSeminarOpposition> studentOppose(final User student, final FinalSeminar finalSeminar, final Project project) {
public Either<OppositionRegistrationErrorStatus, FinalSeminarOpposition> attemptAddOpposition(final User student, final FinalSeminar finalSeminar, final Project project) {
return canOppose(student, finalSeminar, project)
.map(allowed -> {
final FinalSeminarOpposition opposition = new FinalSeminarOpposition();
opposition.setUser(student);
opposition.setFinalSeminar(finalSeminar);
opposition.setProject(project);
finalSeminar.addOpposition(opposition);
finalSeminarRepository.save(finalSeminar);
return opposition;
});
.map(allowed -> createAndSaveOpposition(student, finalSeminar, project));
}
@Override
@Transactional
public se.su.dsv.scipro.util.Either<OpposeError, FinalSeminarOpposition> oppose(User student, FinalSeminar finalSeminar, Project project) {
if (!project.isParticipant(student)) return se.su.dsv.scipro.util.Either.left(OpposeError.NotAuthor);
else if (finalSeminar.getActiveParticipants().contains(student)) return se.su.dsv.scipro.util.Either.left(OpposeError.AlreadyParticipant);
else if (finalSeminar.getOpponents().contains(student)) return se.su.dsv.scipro.util.Either.left(OpposeError.AlreadyOpponent);
else {
FinalSeminarOpposition opposition = new FinalSeminarOpposition();
opposition.setUser(student);
opposition.setFinalSeminar(finalSeminar);
opposition.setProject(project);
finalSeminar.addOpposition(opposition);
finalSeminarRepository.save(finalSeminar);
return se.su.dsv.scipro.util.Either.right(opposition);
public Either<OpposeError, FinalSeminarOpposition> SupervisorAttemptAddOpposition(User student, FinalSeminar finalSeminar, Project project) {
if (!project.isParticipant(student)) {
return Either.left(OpposeError.NotAuthorOfSameProjectType);
} else if (finalSeminar.getActiveParticipants().contains(student)) {
return Either.left(OpposeError.AlreadyParticipant);
} else if (finalSeminar.getOpponents().contains(student)) {
return Either.left(OpposeError.AlreadyOpponent);
} else {
return Either.right(createAndSaveOpposition(student, finalSeminar, project));
}
}
private FinalSeminarOpposition createAndSaveOpposition(User student, FinalSeminar finalSeminar, Project project) {
FinalSeminarOpposition opposition = new FinalSeminarOpposition();
opposition.setUser(student);
opposition.setFinalSeminar(finalSeminar);
opposition.setProject(project);
finalSeminar.addOpposition(opposition);
finalSeminarRepository.save(finalSeminar);
return opposition;
}
@Override
public boolean canParticipate(User user, FinalSeminar finalSeminar) {
return finalSeminar.getActiveParticipations().size() < finalSeminar.getMaxParticipants() &&
finalSeminar.getStartDate().after(new Date()) &&
notAlreadyInSeminar(user, finalSeminar) &&
!finalSeminar.isDeleted();
@Transactional
public Either<ActiveParticipationRegistrationStatus, FinalSeminarActiveParticipation> attemptAddActiveParticipation(final User student, final FinalSeminar finalSeminar, final Project project) {
return canActiveParticipate(student, finalSeminar)
.map(allowed -> createAndSaveActiveParticipation(student, finalSeminar, project));
}
@Override
@Transactional
public Either<ParticipateError, FinalSeminarActiveParticipation> SupervisorAttemptAddActiveParticipation(User student, FinalSeminar finalSeminar, Project project) {
if (!project.isParticipant(student)) {
return Either.left(ParticipateError.NotAuthorOfSameProjectType);
} else if (finalSeminar.getActiveParticipants().contains(student)) {
return Either.left(ParticipateError.AlreadyParticipant);
} else if (finalSeminar.getOpponents().contains(student)) {
return Either.left(ParticipateError.AlreadyOpponent);
} else {
return Either.right(createAndSaveActiveParticipation(student, finalSeminar, project));
}
}
private FinalSeminarActiveParticipation createAndSaveActiveParticipation(User student, FinalSeminar finalSeminar, Project project) {
FinalSeminarActiveParticipation activeParticipation = new FinalSeminarActiveParticipation();
activeParticipation.setUser(student);
activeParticipation.setFinalSeminar(finalSeminar);
activeParticipation.setProject(project);
finalSeminar.addActiveParticipant(activeParticipation);
finalSeminarRepository.save(finalSeminar);
return activeParticipation;
}
@Override
@ -395,11 +428,6 @@ public class FinalSeminarServiceImpl extends AbstractServiceImpl<FinalSeminar, L
return findAll(projectOpposing(project));
}
@Override
public boolean isActiveParticipant(FinalSeminar seminar, User activeParticipant) {
return alreadyParticipant(activeParticipant, seminar);
}
@Override
public boolean hasHadFinalSeminar(Project project) {
FinalSeminar finalSeminar = findByProject(project);
@ -427,7 +455,7 @@ public class FinalSeminarServiceImpl extends AbstractServiceImpl<FinalSeminar, L
@Override
public List<FinalSeminarOpposition> findUserOpposing(Project project, User user) {
return finalSeminarOppositionRepository.findByOpposingUserAndLevel(user, project.getProjectType());
return finalSeminarOppositionRepository.findByOpposingUserAndType(user, project.getProjectType());
}
@Override

@ -1,5 +1,5 @@
package se.su.dsv.scipro.finalseminar;
public enum OpposeError {
AlreadyOpponent, AlreadyParticipant, NotAuthor
AlreadyOpponent, AlreadyParticipant, NotAuthorOfSameProjectType
}

@ -3,8 +3,9 @@ package se.su.dsv.scipro.finalseminar;
import java.time.Instant;
import java.util.function.Function;
public abstract class OppositionRegistrationStatus {
OppositionRegistrationStatus() { }
public abstract class OppositionRegistrationErrorStatus {
OppositionRegistrationErrorStatus() {
}
public abstract <A> A fold(
Function<UngradedOpposition, A> a,
@ -13,10 +14,11 @@ public abstract class OppositionRegistrationStatus {
Function<AlreadyHappened, A> d,
Function<TooManyOpponents, A> e,
Function<PriorityForSeminarAuthors, A> f,
Function<AlreadyOpposed, A> g);
Function<AlreadyOpposed, A> g,
Function<ManualOpponents, A> h);
}
final class UngradedOpposition extends OppositionRegistrationStatus {
final class UngradedOpposition extends OppositionRegistrationErrorStatus {
@Override
public <A> A fold(
final Function<UngradedOpposition, A> a,
@ -25,13 +27,13 @@ final class UngradedOpposition extends OppositionRegistrationStatus {
final Function<AlreadyHappened, A> d,
final Function<TooManyOpponents, A> e,
final Function<PriorityForSeminarAuthors, A> f,
final Function<AlreadyOpposed, A> g)
{
final Function<AlreadyOpposed, A> g,
final Function<ManualOpponents, A> h) {
return a.apply(this);
}
}
final class AlreadyOpposed extends OppositionRegistrationStatus {
final class AlreadyOpposed extends OppositionRegistrationErrorStatus {
@Override
public <A> A fold(
final Function<UngradedOpposition, A> a,
@ -40,13 +42,13 @@ final class AlreadyOpposed extends OppositionRegistrationStatus {
final Function<AlreadyHappened, A> d,
final Function<TooManyOpponents, A> e,
final Function<PriorityForSeminarAuthors, A> f,
final Function<AlreadyOpposed, A> g)
{
final Function<AlreadyOpposed, A> g,
final Function<ManualOpponents, A> h) {
return g.apply(this);
}
}
final class FinalSeminarCancelled extends OppositionRegistrationStatus {
final class FinalSeminarCancelled extends OppositionRegistrationErrorStatus {
@Override
public <A> A fold(
final Function<UngradedOpposition, A> a,
@ -55,13 +57,13 @@ final class FinalSeminarCancelled extends OppositionRegistrationStatus {
final Function<AlreadyHappened, A> d,
final Function<TooManyOpponents, A> e,
final Function<PriorityForSeminarAuthors, A> f,
final Function<AlreadyOpposed, A> g)
{
final Function<AlreadyOpposed, A> g,
final Function<ManualOpponents, A> h) {
return b.apply(this);
}
}
final class AlreadyParticipating extends OppositionRegistrationStatus {
final class AlreadyParticipating extends OppositionRegistrationErrorStatus {
@Override
public <A> A fold(
final Function<UngradedOpposition, A> a,
@ -70,13 +72,13 @@ final class AlreadyParticipating extends OppositionRegistrationStatus {
final Function<AlreadyHappened, A> d,
final Function<TooManyOpponents, A> e,
final Function<PriorityForSeminarAuthors, A> f,
final Function<AlreadyOpposed, A> g)
{
final Function<AlreadyOpposed, A> g,
final Function<ManualOpponents, A> h) {
return c.apply(this);
}
}
final class AlreadyHappened extends OppositionRegistrationStatus {
final class AlreadyHappened extends OppositionRegistrationErrorStatus {
@Override
public <A> A fold(
final Function<UngradedOpposition, A> a,
@ -85,13 +87,13 @@ final class AlreadyHappened extends OppositionRegistrationStatus {
final Function<AlreadyHappened, A> d,
final Function<TooManyOpponents, A> e,
final Function<PriorityForSeminarAuthors, A> f,
final Function<AlreadyOpposed, A> g)
{
final Function<AlreadyOpposed, A> g,
final Function<ManualOpponents, A> h) {
return d.apply(this);
}
}
final class TooManyOpponents extends OppositionRegistrationStatus {
final class TooManyOpponents extends OppositionRegistrationErrorStatus {
@Override
public <A> A fold(
final Function<UngradedOpposition, A> a,
@ -100,13 +102,28 @@ final class TooManyOpponents extends OppositionRegistrationStatus {
final Function<AlreadyHappened, A> d,
final Function<TooManyOpponents, A> e,
final Function<PriorityForSeminarAuthors, A> f,
final Function<AlreadyOpposed, A> g)
{
final Function<AlreadyOpposed, A> g,
final Function<ManualOpponents, A> h) {
return e.apply(this);
}
}
final class PriorityForSeminarAuthors extends OppositionRegistrationStatus {
final class ManualOpponents extends OppositionRegistrationErrorStatus {
@Override
public <A> A fold(
final Function<UngradedOpposition, A> a,
final Function<FinalSeminarCancelled, A> b,
final Function<AlreadyParticipating, A> c,
final Function<AlreadyHappened, A> d,
final Function<TooManyOpponents, A> e,
final Function<PriorityForSeminarAuthors, A> f,
final Function<AlreadyOpposed, A> g,
final Function<ManualOpponents, A> h) {
return h.apply(this);
}
}
final class PriorityForSeminarAuthors extends OppositionRegistrationErrorStatus {
private final int priorityDays;
private final Instant priorityEnd;
@ -131,8 +148,8 @@ final class PriorityForSeminarAuthors extends OppositionRegistrationStatus {
final Function<AlreadyHappened, A> d,
final Function<TooManyOpponents, A> e,
final Function<PriorityForSeminarAuthors, A> f,
final Function<AlreadyOpposed, A> g)
{
final Function<AlreadyOpposed, A> g,
final Function<ManualOpponents, A> h) {
return f.apply(this);
}
}

@ -0,0 +1,5 @@
package se.su.dsv.scipro.finalseminar;
public enum ParticipateError {
AlreadyOpponent, AlreadyParticipant, NotAuthorOfSameProjectType
}

@ -242,16 +242,6 @@ public class Project extends DomainObject {
return getReviewer() != null ? getReviewer().getFullName() : NO_REVIEWER;
}
public String getCoSupervisorName() {
if (!getCoSupervisors().isEmpty()) {
return getCoSupervisors()
.stream()
.map(User::getFullName)
.collect(Collectors.joining(", "));
}
return NO_CO_SUPERVISOR;
}
public DegreeType getProjectTypeDegreeType() {
return getProjectType().getDegreeType();
}

@ -111,30 +111,30 @@ public class FinalSeminarServiceImplIntegrationTest extends IntegrationTest {
@Test
public void can_not_oppose_if_already_opponent() {
otherProject.addProjectParticipant(user);
finalSeminarService.oppose(user, futureFinalSeminar, otherProject);
finalSeminarService.SupervisorAttemptAddOpposition(user, futureFinalSeminar, otherProject);
assertThat(finalSeminarService.canOppose(user, futureFinalSeminar, otherProject), isLeft(instanceOf(AlreadyParticipating.class)));
}
@Test
public void can_not_oppose_if_participant() {
finalSeminarService.participate(user, futureFinalSeminar, otherProject);
finalSeminarService.SupervisorAttemptAddActiveParticipation(user, futureFinalSeminar, otherProject);
assertThat(finalSeminarService.canOppose(user, futureFinalSeminar, otherProject), isLeft(instanceOf(AlreadyParticipating.class)));
}
@Test
public void can_participate_on_future_seminar() {
assertTrue(finalSeminarService.canParticipate(user, futureFinalSeminar));
assertThat(finalSeminarService.canActiveParticipate(user, futureFinalSeminar), isRight(anything()));
}
@Test
public void can_not_participate_on_past_seminar() {
assertFalse(finalSeminarService.canParticipate(user, pastFinalSeminar));
assertThat(finalSeminarService.canActiveParticipate(user, pastFinalSeminar), isLeft(instanceOf(ParticipationAlreadyHappened.class)));
}
@Test
public void can_not_participate_twice() {
finalSeminarService.participate(user, futureFinalSeminar, otherProject);
assertFalse(finalSeminarService.canParticipate(user, futureFinalSeminar));
finalSeminarService.SupervisorAttemptAddActiveParticipation(user, futureFinalSeminar, otherProject);
assertThat(finalSeminarService.canActiveParticipate(user, pastFinalSeminar), isLeft(instanceOf(ParticipationAlreadyParticipating.class)));
}
@Test
@ -146,7 +146,7 @@ public class FinalSeminarServiceImplIntegrationTest extends IntegrationTest {
@Test
public void can_not_participate_if_max_is_reached() {
futureFinalSeminar.setMaxParticipants(0);
assertFalse(finalSeminarService.canParticipate(user, futureFinalSeminar));
assertThat(finalSeminarService.canActiveParticipate(user, pastFinalSeminar), isLeft(instanceOf(ManualParticipants.class)));
}
@Test
@ -158,7 +158,7 @@ public class FinalSeminarServiceImplIntegrationTest extends IntegrationTest {
@Test
public void can_not_participate_if_is_author() {
futureFinalSeminar.getProject().addProjectParticipant(user);
assertFalse(finalSeminarService.canParticipate(user, futureFinalSeminar));
assertThat(finalSeminarService.canActiveParticipate(user, pastFinalSeminar), isLeft(instanceOf(ParticipationAlreadyParticipating.class)));
}
@Test
@ -170,13 +170,13 @@ public class FinalSeminarServiceImplIntegrationTest extends IntegrationTest {
@Test
public void can_not_participate_on_deleted_seminar() {
futureFinalSeminar.setDeleted(true);
assertFalse(finalSeminarService.canParticipate(user, futureFinalSeminar));
assertThat(finalSeminarService.canActiveParticipate(user, pastFinalSeminar), isLeft(instanceOf(ParticipationFinalSeminarCancelled.class)));
}
@Test
public void can_participate_if_previous_oppositions_are_failed() {
FinalSeminar secondSeminar = createFinalSeminar(createProject(), 10);
finalSeminarService.oppose(user, futureFinalSeminar, otherProject);
finalSeminarService.SupervisorAttemptAddOpposition(user, futureFinalSeminar, otherProject);
for (FinalSeminarOpposition finalSeminarOpposition : futureFinalSeminar.getOppositions()) {
finalSeminarOpposition.setGrade(FinalSeminarGrade.NOT_APPROVED);
}

@ -29,7 +29,7 @@ public abstract class OpposeColumnPanel extends Panel {
public OpposeColumnPanel(String id, final IModel<FinalSeminar> finalSeminarModel, final IModel<Project> projectModel) {
super(id, finalSeminarModel);
final Either<OppositionRegistrationStatus, Void> canOppose =
final Either<OppositionRegistrationErrorStatus, Void> canOppose =
finalSeminarService.canOppose(SciProSession.get().getUser(), finalSeminarModel.getObject(), projectModel.getObject());
final Component opposeLink = canOppose.fold(
this::showError,
@ -39,7 +39,7 @@ public abstract class OpposeColumnPanel extends Panel {
add(opposeLink);
}
private Component showError(OppositionRegistrationStatus notAllowed) {
private Component showError(OppositionRegistrationErrorStatus notAllowed) {
return notAllowed.fold(
ungradedOpposition -> getLabel("You have ungraded oppositions"),
finalSeminarCancelled -> getLabel("The final seminar has been cancelled"),
@ -49,7 +49,8 @@ public abstract class OpposeColumnPanel extends Panel {
priorityForSeminarAuthors -> getLabel("Authors with their own final seminar have " +
"priority to register for " + priorityForSeminarAuthors.getPriorityDays() +
" day(s). This priority will end at " + asDateTime(Date.from(priorityForSeminarAuthors.getPriorityEnd()))),
alreadyOpposed -> getLabel("You have already completed an opposition and can not perform a second one."));
alreadyOpposed -> getLabel("You have already completed an opposition and can not perform a second one."),
manualOpponents -> getLabel("Opponents for this seminar are handled by the supervisor."));
}
public String asDateTime(Date date) {

@ -1,11 +1,14 @@
package se.su.dsv.scipro.finalseminar;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.Component;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.link.StatelessLink;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import se.su.dsv.scipro.session.SciProSession;
import se.su.dsv.scipro.util.AjaxConfirmationLink;
import se.su.dsv.scipro.util.Either;
import se.su.dsv.scipro.util.JavascriptEventConfirmation;
import javax.inject.Inject;
@ -17,32 +20,55 @@ public abstract class ParticipateColumnPanel extends Panel {
@Inject
private FinalSeminarService finalSeminarService;
private AjaxLink participateLink;
private ParticipateColumnPanel participateColumnPanel;
public abstract void onClick(IModel<FinalSeminar> clickedModel);
public ParticipateColumnPanel(String id, final IModel<FinalSeminar> model) {
super(id, model);
public ParticipateColumnPanel(String id, final IModel<FinalSeminar> finalSeminarModel) {
super(id, finalSeminarModel);
participateColumnPanel = this;
final Either<ActiveParticipationRegistrationStatus, Void> canActiveParticipate =
finalSeminarService.canActiveParticipate(SciProSession.get().getUser(), finalSeminarModel.getObject());
final Component participateLink = canActiveParticipate.fold(
this::showError,
allowed -> new ParticipateColumnPanel.ParticipateLink(LINK, finalSeminarModel)
);
participateLink = new AjaxConfirmationLink<Void>(LINK, PARTICIPATE_CONFIRMATION) {
@Override
public void onClick(AjaxRequestTarget target) {
participateColumnPanel.onClick(model);
participateLink.setEnabled(false);
target.add(participateLink);
}
@Override
protected void onConfigure() {
super.onConfigure();
setEnabled(finalSeminarService.canParticipate(SciProSession.get().getUser(), model.getObject()));
}
};
participateLink.setOutputMarkupId(true);
add(participateLink);
}
}
private Component showError(ActiveParticipationRegistrationStatus notAllowed) {
return notAllowed.fold(
tooManyParticipants -> getLabel("The seminar is already has the maximum participants"),
ManualParticipants -> getLabel("Participants for this seminar are handled by the supervisor."),
ParticipationAlreadyParticipating -> getLabel("You are already participating in the final seminar"),
ParticipationAlreadyHappened -> getLabel("The seminar has already happened"),
ParticipationFinalSeminarCancelled -> getLabel("The final seminar has been cancelled"));
}
private Component getLabel(final String label) {
// Dumb workaround to get the <a> tag transformed into a <span>
final StatelessLink<Void> components = new StatelessLink<>(LINK) {
@Override
public void onClick() {
}
};
return components
.setBody(Model.of(label))
.setEnabled(false);
}
private class ParticipateLink extends Link<FinalSeminar> {
public ParticipateLink(final String id, final IModel<FinalSeminar> finalSeminarModel) {
super(id, finalSeminarModel);
add(new JavascriptEventConfirmation("click", ParticipateColumnPanel.PARTICIPATE_CONFIRMATION));
setOutputMarkupId(true);
}
@Override
public void onClick() {
ParticipateColumnPanel.this.onClick(getModel());
}
}
}

@ -94,8 +94,8 @@ public class ProjectOppositionPage extends AbstractProjectDetailsPage implements
add(new ExportableDataPanel<>("select", createColumns(), provider));
}
public boolean oppose(FinalSeminar finalSeminar) {
return finalSeminarService.studentOppose(SciProSession.get().getUser(), finalSeminar, getActiveProject())
public boolean submitOpposition(FinalSeminar finalSeminar) {
return finalSeminarService.attemptAddOpposition(SciProSession.get().getUser(), finalSeminar, getActiveProject())
.fold(
notAllowed -> {
final String errorMessage = notAllowed.fold(
@ -108,7 +108,8 @@ public class ProjectOppositionPage extends AbstractProjectDetailsPage implements
"Authors with their own final seminar have " +
"priority to register for " + priorityForSeminarAuthors.getPriorityDays() +
" day(s). This priority will end at " + asDateTime(Date.from(priorityForSeminarAuthors.getPriorityEnd())),
alreadyOpposed -> "You have already completed an opposition and can not perform a second one."
alreadyOpposed -> "You have already completed an opposition and can't perform a second one.",
manualOpponents -> "Opponents for this seminar are handled by the supervisor."
);
error(errorMessage);
return Boolean.FALSE;
@ -125,16 +126,30 @@ public class ProjectOppositionPage extends AbstractProjectDetailsPage implements
return dateFormat.format(date);
}
public void participate(FinalSeminar finalSeminar) {
finalSeminarService.participate(SciProSession.get().getUser(), finalSeminar, getActiveProject());
notificationController.notifySeminar(finalSeminar, SeminarEvent.Event.PARTICIPATION_CHANGED, new NotificationSource());
public boolean submitParticipation(FinalSeminar finalSeminar) {
return finalSeminarService.attemptAddActiveParticipation(SciProSession.get().getUser(), finalSeminar, getActiveProject())
.fold(
notAllowed -> {
final String errorMessage = notAllowed.fold(
TooManyParticipants -> "The seminar is already full on participants",
ManualParticipants -> "Participants for this seminar are handled by the supervisor.",
ParticipationAlreadyParticipating -> "You are already participating in the final seminar",
ParticipationAlreadyHappened -> "The seminar has already happened",
ParticipationFinalSeminarCancelled -> "The final seminar has been cancelled");
error(errorMessage);
return Boolean.FALSE;
},
opposed -> {
notificationController.notifySeminar(finalSeminar, SeminarEvent.Event.PARTICIPATION_CHANGED, new NotificationSource());
return Boolean.TRUE;
}
);
}
private List<IColumn<FinalSeminar, String>> createColumns() {
List<IColumn<FinalSeminar, String>> columns = new ArrayList<>();
columns.add(new DateColumn<>(Model.of("Date"), FinalSeminar::getStartDate, "startDate", DateStyle.DATETIME));
columns.add(new LambdaColumn<>(Model.of("Type"), "project.projectType.name", fs -> fs.getProjectType().getName()));
columns.add(new AbstractColumn<>(Model.of("Title"), "project.title") {
@Override
public void populateItem(Item<ICellPopulator<FinalSeminar>> cellItem, String componentId, final IModel<FinalSeminar> rowModel) {
@ -154,8 +169,8 @@ public class ProjectOppositionPage extends AbstractProjectDetailsPage implements
item.add(new OpposeColumnPanel(s, iModel, projectModel) {
@Override
public void onClick(IModel<FinalSeminar> clickedModel) {
if (oppose(clickedModel.getObject())) {
ProjectOppositionPage.this.success(getString("oppose", clickedModel));
if (submitOpposition(clickedModel.getObject())) {
ProjectOppositionPage.this.success(getString("oppositionAdded", clickedModel));
}
}
});
@ -167,9 +182,9 @@ public class ProjectOppositionPage extends AbstractProjectDetailsPage implements
item.add(new ParticipateColumnPanel(s, iModel) {
@Override
public void onClick(IModel<FinalSeminar> clickedModel) {
participate(clickedModel.getObject());
getSession().success(getString("participate", clickedModel));
setResponsePage(ProjectOppositionPage.class);
if (submitParticipation(clickedModel.getObject())) {
ProjectOppositionPage.this.success(getString("participationAdded", clickedModel));
}
}
});
}

@ -1,4 +1,4 @@
info.text= If the Oppose and Participate links are black and not clickable that means the \
seminar is full. Wait for another seminar to oppose on or participate in.
oppose= You are now registered as opponent on the final seminar for project: ${projectTitle}
participate= You are now registered as an active participant on the final seminar for project: ${projectTitle}
oppositionAdded= You are now registered as an opponent for the final seminar for project: ${projectTitle}
participationAdded= You are now registered as an active participant for the final seminar for project: ${projectTitle}

@ -45,7 +45,7 @@ public class SeminarCRUDPanel extends GenericPanel<FinalSeminar> {
@Inject private NonWorkDayPeriodService nonWorkDays;
private DefaultSelect2MultiChoice<User> opponents;
private DefaultSelect2MultiChoice<User> participants;
private DefaultSelect2MultiChoice<User> activeParticipants;
private Set<SeminarEvent.Event> events = new HashSet<>();
public SeminarCRUDPanel(String id, IModel<FinalSeminar> seminar) {
@ -104,8 +104,8 @@ public class SeminarCRUDPanel extends GenericPanel<FinalSeminar> {
final AutoCompleteRoleProvider authorChoices = new AutoCompleteRoleProvider(userSearchService, Set.of(Roles.AUTHOR));
opponents = new DefaultSelect2MultiChoice<>("opponents", new DetachableServiceModelCollection<>(userService), authorChoices);
add(opponents);
participants = new DefaultSelect2MultiChoice<>("participants", new DetachableServiceModelCollection<>(userService), authorChoices);
add(participants);
activeParticipants = new DefaultSelect2MultiChoice<>("participants", new DetachableServiceModelCollection<>(userService), authorChoices);
add(activeParticipants);
add(new ConfirmationLink<>(DELETE, seminar, new StringResourceModel("confirm", this)) {
@Override
@ -143,11 +143,15 @@ public class SeminarCRUDPanel extends GenericPanel<FinalSeminar> {
if (seminar.getActiveParticipants().contains(opponent)) {
error("Selected opponent is already active participant");
}
if (participants.getConvertedInput().contains(opponent)) {
if (activeParticipants.getConvertedInput().contains(opponent)) {
error("Can't add same user as both opponent and active participant");
}
if (hasError()) {
opponents.clearInput();
opponents.getModel().setObject(Collections.emptyList());
}
}
for (User participant : participants.getConvertedInput()) {
for (User participant : activeParticipants.getConvertedInput()) {
if (seminar.getProject().getHeadSupervisor().equals(participant)) {
error("Head supervisor can't be active participant");
}
@ -166,6 +170,10 @@ public class SeminarCRUDPanel extends GenericPanel<FinalSeminar> {
if (opponents.getConvertedInput().contains(participant)) {
error("Can't add same user as both opponent and active participant");
}
if (hasError()) {
activeParticipants.clearInput();
activeParticipants.getModel().setObject(Collections.emptyList());
}
}
}
@ -193,24 +201,35 @@ public class SeminarCRUDPanel extends GenericPanel<FinalSeminar> {
}
}
private void addActiveParticipants(FinalSeminar seminar) {
for (User participant : participants.getModelObject()) {
if (!seminarService.isActiveParticipant(seminar, participant)) {
List<Project> participantProjects = projectService.getActiveProjects(participant, seminar.getProjectType());
if (participantProjects.isEmpty()) {
error(getString("opponent.no.project", Model.of(participant)));
} else if (participantProjects.size() > 1) {
error(getString("opponent.too.many.projects", Model.of(participant)));
} else {
FinalSeminarActiveParticipation participation = new FinalSeminarActiveParticipation();
participation.setUser(participant);
participation.setFinalSeminar(seminar);
participation.setProject(participantProjects.get(0));
seminar.addActiveParticipant(participation);
events.add(SeminarEvent.Event.PARTICIPATION_CHANGED);
}
private void addActiveParticipants(FinalSeminar finalSeminar) {
for (User potentialParticipant : activeParticipants.getModelObject()) {
Optional<Project> maybeProject = getPotentialParticipantProject(potentialParticipant, finalSeminar.getProjectType());
if (maybeProject.isEmpty()) {
error(getString("opponent.no.project", Model.of(potentialParticipant)));
} else {
final Project project = maybeProject.get();
Either<ParticipateError, FinalSeminarActiveParticipation> result = seminarService.SupervisorAttemptAddActiveParticipation(potentialParticipant, finalSeminar, project);
result.fold(
error -> {
switch (error) {
case AlreadyOpponent ->
error("Selected opponent " + potentialParticipant.getFullName() + " is already opponent");
case AlreadyParticipant ->
error("Selected opponent " + potentialParticipant.getFullName() + " is already active participant");
case NotAuthorOfSameProjectType ->
error("Failed to add " + potentialParticipant.getFullName() + ", not an author of a project of same type");
}
return false;
},
opponent -> {
success("Added " + potentialParticipant.getFullName() + " as an opponent");
events.add(SeminarEvent.Event.PARTICIPATION_CHANGED);
return true;
});
}
}
activeParticipants.clearInput();
activeParticipants.getModel().setObject(Collections.emptyList());
}
private void addOpponentsTo(FinalSeminar finalSeminar) {
@ -221,7 +240,7 @@ public class SeminarCRUDPanel extends GenericPanel<FinalSeminar> {
error(getString("opponent.no.project", Model.of(potentialOpponent)));
} else {
final Project project = maybeProject.get();
Either<OpposeError, FinalSeminarOpposition> result = seminarService.oppose(potentialOpponent, finalSeminar, project);
Either<OpposeError, FinalSeminarOpposition> result = seminarService.SupervisorAttemptAddOpposition(potentialOpponent, finalSeminar, project);
result.fold(
error -> {
switch (error) {
@ -229,8 +248,8 @@ public class SeminarCRUDPanel extends GenericPanel<FinalSeminar> {
error("Selected opponent " + potentialOpponent.getFullName() + " is already opponent");
case AlreadyParticipant ->
error("Selected opponent " + potentialOpponent.getFullName() + " is already active participant");
case NotAuthor ->
error("Failed to add " + potentialOpponent.getFullName() + " as an opponent");
case NotAuthorOfSameProjectType ->
error("Failed to add " + potentialOpponent.getFullName() + ", not an author of a project of same type");
}
return false;
},
@ -240,6 +259,8 @@ public class SeminarCRUDPanel extends GenericPanel<FinalSeminar> {
return true;
});
}
opponents.clearInput();
opponents.getModel().setObject(Collections.emptyList());
}
}

@ -4,8 +4,8 @@ maxOpponents.RangeValidator.minimum=The selected number of max oppositions may n
FinalSeminarLanguage.SWEDISH=Swedish
# suppress inspection "UnusedProperty"
FinalSeminarLanguage.ENGLISH=English
opponent.no.project=${fullName} does not have an active project on this projects type.
opponent.too.many.projects=${fullName} has more than one active project on this projects type - unable to add opponent manually.
opponent.no.project=${fullName} does not have an active project of this projects type.
opponent.too.many.projects=${fullName} has more than one active project of this projects type - unable to add opponent manually.
final.seminar.updated=Final seminar saved
create= Create
update= Update

@ -64,19 +64,19 @@ public class ProjectOppositionPageTest extends PageTest {
Mockito.when(finalSeminarService.findAll(ArgumentMatchers.any(FinalSeminarService.Filter.class), ArgumentMatchers.any(Pageable.class))).thenReturn(Collections.singletonList(finalSeminar));
Mockito.when(finalSeminarService.count(ArgumentMatchers.any(FinalSeminarService.Filter.class))).thenReturn(1L);
Mockito.when(finalSeminarService.canOppose(user, finalSeminar, project)).thenReturn(Either.right(null));
Mockito.when(finalSeminarService.canParticipate(user, finalSeminar)).thenReturn(true);
Mockito.when(finalSeminarService.canActiveParticipate(user, finalSeminar)).thenReturn(Either.right(null));
tester.startPage(ProjectOppositionPage.class, pageParameters);
}
@Test
public void clickingOpposeUpdatesCorrectProject() {
Mockito.when(finalSeminarService.studentOppose(user, finalSeminar, project)).thenReturn(Either.right(null));
Mockito.when(finalSeminarService.attemptAddOpposition(user, finalSeminar, project)).thenReturn(Either.right(null));
final Page page = tester.getLastRenderedPage();
page.visitChildren(OpposeColumnPanel.class, new IVisitor<Component, Object>() {
@Override
public void component(Component component, IVisit<Object> visit) {
tester.clickLink(path(component.getPageRelativePath(), OpposeColumnPanel.LINK));
Mockito.verify(finalSeminarService).studentOppose(user, finalSeminar, project);
Mockito.verify(finalSeminarService).attemptAddOpposition(user, finalSeminar, project);
visit.stop();
}
});
@ -84,7 +84,7 @@ public class ProjectOppositionPageTest extends PageTest {
@Test
public void clicking_oppose_generates_feedback() {
Mockito.when(finalSeminarService.studentOppose(user, finalSeminar, project)).thenReturn(Either.right(null));
Mockito.when(finalSeminarService.attemptAddOpposition(user, finalSeminar, project)).thenReturn(Either.right(null));
final Page page = tester.getLastRenderedPage();
page.visitChildren(OpposeColumnPanel.class, new IVisitor<Component, Object>() {
@Override
@ -92,7 +92,7 @@ public class ProjectOppositionPageTest extends PageTest {
tester.clickLink(path(component.getPageRelativePath(), OpposeColumnPanel.LINK));
List<Serializable> messages = tester.getMessages(FeedbackMessage.SUCCESS);
MatcherAssert.assertThat(messages, hasItem(StringContains.containsString(page.getString("oppose", Model.of(finalSeminar)))));
MatcherAssert.assertThat(messages, hasItem(StringContains.containsString(page.getString("oppositionAdded", Model.of(finalSeminar)))));
visit.stop();
}
});
@ -105,7 +105,7 @@ public class ProjectOppositionPageTest extends PageTest {
@Override
public void component(Component component, IVisit<Object> visit) {
tester.clickLink(path(component.getPageRelativePath(), ParticipateColumnPanel.LINK));
Mockito.verify(finalSeminarService).participate(user, finalSeminar, project);
Mockito.verify(finalSeminarService).SupervisorAttemptAddActiveParticipation(user, finalSeminar, project);
visit.stop();
}
});
@ -120,7 +120,7 @@ public class ProjectOppositionPageTest extends PageTest {
tester.clickLink(path(component.getPageRelativePath(), ParticipateColumnPanel.LINK));
List<Serializable> messages = tester.getMessages(FeedbackMessage.SUCCESS);
MatcherAssert.assertThat(messages, hasItem(StringContains.containsString(page.getString("participate", Model.of(finalSeminar)))));
MatcherAssert.assertThat(messages, hasItem(StringContains.containsString(page.getString("participationAdded", Model.of(finalSeminar)))));
visit.stop();
}
});