Merge branch 'develop' into wicket-10

# Conflicts:
#	view/src/main/java/se/su/dsv/scipro/supervisor/pages/SupervisorViewGroupThreadPage.java
This commit is contained in:
Andreas Svanberg 2024-04-16 14:13:03 +02:00
commit 42cd644e74
48 changed files with 370 additions and 130 deletions
core/src
owasp.xml
view

@ -71,6 +71,7 @@ public class FinalSeminarServiceImpl extends AbstractServiceImpl<FinalSeminar, L
} }
@Override @Override
@Transactional
public Either<SchedulingError, FinalSeminar> schedule(Project project, LocalDateTime when, FinalSeminarDetails details) { public Either<SchedulingError, FinalSeminar> schedule(Project project, LocalDateTime when, FinalSeminarDetails details) {
if (project.isFinalSeminarRuleExempted()) { if (project.isFinalSeminarRuleExempted()) {
return createSeminar(project, when, details); return createSeminar(project, when, details);
@ -86,7 +87,14 @@ public class FinalSeminarServiceImpl extends AbstractServiceImpl<FinalSeminar, L
return Either.left(new RoughDraftNotApproved()); return Either.left(new RoughDraftNotApproved());
} }
return createSeminar(project, when, details); final FinalSeminar current = findByProject(project);
if (current == null) {
return createSeminar(project, when, details);
}
else {
// Assume double click sends the same data so no need to change anything
return Either.right(current);
}
} }
private MovingError validateSchedulingRules(LocalDate date) { private MovingError validateSchedulingRules(LocalDate date) {

@ -51,8 +51,11 @@ public class BasicForumServiceImpl implements BasicForumService {
@Override @Override
@Transactional @Transactional
public boolean setThreadRead(User user, ForumThread forumThread, boolean read) { public boolean setThreadRead(User user, ForumThread forumThread, boolean read) {
for (ForumPost post : forumThread.getPosts()) { readStateRepository.setThreadRead(user, forumThread, read);
setRead(user, post, read); if (read) {
for (ForumPost post : forumThread.getPosts()) {
eventBus.post(new ForumPostReadEvent(post, user));
}
} }
return read; return read;
} }

@ -3,20 +3,5 @@ package se.su.dsv.scipro.forum;
import se.su.dsv.scipro.forum.dataobjects.ForumPost; import se.su.dsv.scipro.forum.dataobjects.ForumPost;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
public final class ForumPostReadEvent { public record ForumPostReadEvent(ForumPost post, User user) {
private final ForumPost post;
private final User user;
public ForumPostReadEvent(final ForumPost post, final User user) {
this.post = post;
this.user = user;
}
public ForumPost getPost() {
return post;
}
public User getUser() {
return user;
}
} }

@ -6,6 +6,7 @@ import org.springframework.data.querydsl.QueryDslPredicateExecutor;
import se.su.dsv.scipro.forum.dataobjects.ForumPost; import se.su.dsv.scipro.forum.dataobjects.ForumPost;
import se.su.dsv.scipro.forum.dataobjects.ForumPostReadState; import se.su.dsv.scipro.forum.dataobjects.ForumPostReadState;
import se.su.dsv.scipro.forum.dataobjects.ForumPostReadStateId; import se.su.dsv.scipro.forum.dataobjects.ForumPostReadStateId;
import se.su.dsv.scipro.forum.dataobjects.ForumThread;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
@Transactional @Transactional
@ -13,4 +14,6 @@ public interface ForumPostReadStateRepository
extends JpaRepository<ForumPostReadState, ForumPostReadStateId>, QueryDslPredicateExecutor<ForumPostReadState> { extends JpaRepository<ForumPostReadState, ForumPostReadStateId>, QueryDslPredicateExecutor<ForumPostReadState> {
ForumPostReadState find(User user, ForumPost post); ForumPostReadState find(User user, ForumPost post);
void setThreadRead(User user, ForumThread forumThread, boolean read);
} }

@ -1,8 +1,11 @@
package se.su.dsv.scipro.forum; package se.su.dsv.scipro.forum;
import com.google.inject.persist.Transactional;
import jakarta.persistence.LockModeType;
import se.su.dsv.scipro.forum.dataobjects.ForumPost; import se.su.dsv.scipro.forum.dataobjects.ForumPost;
import se.su.dsv.scipro.forum.dataobjects.ForumPostReadState; import se.su.dsv.scipro.forum.dataobjects.ForumPostReadState;
import se.su.dsv.scipro.forum.dataobjects.ForumPostReadStateId; import se.su.dsv.scipro.forum.dataobjects.ForumPostReadStateId;
import se.su.dsv.scipro.forum.dataobjects.ForumThread;
import se.su.dsv.scipro.forum.dataobjects.QForumPostReadState; import se.su.dsv.scipro.forum.dataobjects.QForumPostReadState;
import se.su.dsv.scipro.system.GenericRepo; import se.su.dsv.scipro.system.GenericRepo;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
@ -23,4 +26,21 @@ public class ForumPostReadStateRepositoryImpl extends GenericRepo<ForumPostReadS
public ForumPostReadState find(User user, ForumPost post) { public ForumPostReadState find(User user, ForumPost post) {
return findOne(allOf(QForumPostReadState.forumPostReadState.id.user.eq(user), QForumPostReadState.forumPostReadState.id.post.eq(post))); return findOne(allOf(QForumPostReadState.forumPostReadState.id.user.eq(user), QForumPostReadState.forumPostReadState.id.post.eq(post)));
} }
@Override
@Transactional
public void setThreadRead(User user, ForumThread forumThread, boolean read) {
EntityManager em = em();
em.lock(forumThread, LockModeType.PESSIMISTIC_WRITE);
for (ForumPost post : forumThread.getPosts()) {
ForumPostReadState state = find(user, post);
if (state == null) {
state = new ForumPostReadState();
state.setId(new ForumPostReadStateId(user, post));
}
state.setRead(read);
em.persist(state);
}
em.lock(forumThread, LockModeType.NONE);
}
} }

@ -97,7 +97,7 @@ public class ForumNotifications {
@Subscribe @Subscribe
@Transactional @Transactional
public void forumPostRead(ForumPostReadEvent forumPostReadEvent) { public void forumPostRead(ForumPostReadEvent forumPostReadEvent) {
forumNotificationRepository.findByForumPost(forumPostReadEvent.getPost()).ifPresent(connection -> forumNotificationRepository.findByForumPost(forumPostReadEvent.post()).ifPresent(connection ->
notificationService.setRead(forumPostReadEvent.getUser(), connection.getNotificationEvent(), true)); notificationService.setRead(forumPostReadEvent.user(), connection.getNotificationEvent(), true));
} }
} }

@ -7,6 +7,9 @@ import java.time.LocalDate;
import java.util.*; import java.util.*;
public interface GradingService { public interface GradingService {
/**
* @return the list of examinations for the given project and author, or {@code null} if the request failed
*/
List<Examination> getExaminations(String token, long projectId, long authorId); List<Examination> getExaminations(String token, long projectId, long authorId);
Either<GetGradeError, Optional<Result>> getResult(String token, long projectId, long authorId, long examinationId); Either<GetGradeError, Optional<Result>> getResult(String token, long projectId, long authorId, long examinationId);

@ -46,7 +46,7 @@ public class GradingServiceImpl implements GradingService {
return response.readEntity(EXAMINATION_LIST); return response.readEntity(EXAMINATION_LIST);
} }
else { else {
return Collections.emptyList(); return null;
} }
} }

@ -6,4 +6,6 @@ public interface PublicationMetadataService {
PublicationMetadata getByProject(Project project); PublicationMetadata getByProject(Project project);
void save(PublicationMetadata publicationMetadata); void save(PublicationMetadata publicationMetadata);
boolean hasSuppliedPublicationMetadata(Project project, boolean noNationalSubjectCategoriesAvailable);
} }

@ -1,6 +1,7 @@
package se.su.dsv.scipro.grading; package se.su.dsv.scipro.grading;
import se.su.dsv.scipro.project.Project; import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.Language;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import java.util.Objects; import java.util.Objects;
@ -29,4 +30,17 @@ class PublicationMetadataServiceImpl implements PublicationMetadataService {
public void save(PublicationMetadata publicationMetadata) { public void save(PublicationMetadata publicationMetadata) {
publicationMetadataRepository.save(publicationMetadata); publicationMetadataRepository.save(publicationMetadata);
} }
@Override
public boolean hasSuppliedPublicationMetadata(Project project, boolean noNationalSubjectCategoriesAvailable) {
final PublicationMetadata metadata = getByProject(project);
return notBlank(metadata.getAbstractEnglish()) &&
(project.getLanguage() == Language.ENGLISH || notBlank(metadata.getAbstractSwedish())) &&
(noNationalSubjectCategoriesAvailable || metadata.getNationalSubjectCategory() != null);
}
private boolean notBlank(String s) {
return s != null && !s.isBlank();
}
} }

@ -45,7 +45,6 @@ public class IdeaServiceImpl extends AbstractServiceImpl<Idea, Long> implements
public static final String NO_LONGER_AVAILABLE_ERROR = "Idea is no longer available"; public static final String NO_LONGER_AVAILABLE_ERROR = "Idea is no longer available";
public static final String ALREADY_PARTICIPATING_ERROR = "You are already participating in another idea"; public static final String ALREADY_PARTICIPATING_ERROR = "You are already participating in another idea";
public static final String PARTNER_ALREADY_PARTICIPATING_ERROR = "Your partner is already participating in another idea"; public static final String PARTNER_ALREADY_PARTICIPATING_ERROR = "Your partner is already participating in another idea";
public static final String SELECTED_IDEA = "You selected idea: ";
public static final String BACHELOR_NEED_PARTNER_ERROR = "You need to select a partner when the idea is on bachelor level"; public static final String BACHELOR_NEED_PARTNER_ERROR = "You need to select a partner when the idea is on bachelor level";
public static final String ADD_SELF_AS_PARTNER_ERROR = "You may not add yourself as project partner"; public static final String ADD_SELF_AS_PARTNER_ERROR = "You may not add yourself as project partner";
public static final String NO_AUTHORS_ERROR = "The idea is submitted by a student, number of students is not allowed"; public static final String NO_AUTHORS_ERROR = "The idea is submitted by a student, number of students is not allowed";
@ -217,7 +216,8 @@ public class IdeaServiceImpl extends AbstractServiceImpl<Idea, Long> implements
return new Pair<>(Boolean.FALSE, WRONG_LEVEL_FOR_YOU); return new Pair<>(Boolean.FALSE, WRONG_LEVEL_FOR_YOU);
} }
return new Pair<>(Boolean.TRUE, SELECTED_IDEA + idea.getTitle()); return new Pair<>(Boolean.TRUE, "You have successfully selected the supervisor idea "
+ idea.getTitle() + ", in the application period " + ap.getName());
} }

@ -6,6 +6,7 @@ import se.su.dsv.scipro.system.ProjectType;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -29,6 +30,10 @@ public abstract class GradingReport extends Report {
@OneToMany(mappedBy = "gradingReport", cascade = {CascadeType.ALL}) @OneToMany(mappedBy = "gradingReport", cascade = {CascadeType.ALL})
private List<GradingCriterion> gradingCriteria = new ArrayList<>(); private List<GradingCriterion> gradingCriteria = new ArrayList<>();
@Basic
@Column(name = "date_submitted_to_examiner")
private Instant dateSubmittedToExaminer;
protected GradingReport() { protected GradingReport() {
// JPA // JPA
} }
@ -37,6 +42,7 @@ public abstract class GradingReport extends Report {
public void submit() { public void submit() {
super.submit(); super.submit();
setState(State.FINALIZED); setState(State.FINALIZED);
setDateSubmittedToExaminer(Instant.now());
} }
public Project getProject() { public Project getProject() {
@ -51,6 +57,14 @@ public abstract class GradingReport extends Report {
gradingCriteria.add(criterion); gradingCriteria.add(criterion);
} }
public Instant getDateSubmittedToExaminer(){
return this.dateSubmittedToExaminer;
}
public void setDateSubmittedToExaminer(Instant dateSubmittedToExaminer) {
this.dateSubmittedToExaminer = dateSubmittedToExaminer;
}
public State getState() { public State getState() {
return state; return state;
} }

@ -7,6 +7,7 @@ import se.su.dsv.scipro.system.GenericService;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.util.Either; import se.su.dsv.scipro.util.Either;
import java.time.Instant;
import java.util.List; import java.util.List;
public interface GradingReportService extends GenericService<GradingReport, Long> { public interface GradingReportService extends GenericService<GradingReport, Long> {
@ -24,4 +25,6 @@ public interface GradingReportService extends GenericService<GradingReport, Long
GradingBasis getGradingBasis(Project project); GradingBasis getGradingBasis(Project project);
GradingBasis updateGradingBasis(Project project, GradingBasis gradingBasis); GradingBasis updateGradingBasis(Project project, GradingBasis gradingBasis);
Instant getDateSentToExaminer(Project project);
} }

@ -16,6 +16,7 @@ import jakarta.inject.Inject;
import jakarta.inject.Named; import jakarta.inject.Named;
import jakarta.inject.Provider; import jakarta.inject.Provider;
import java.time.Clock; import java.time.Clock;
import java.time.Instant;
import java.util.*; import java.util.*;
@Named @Named
@ -92,6 +93,16 @@ public class GradingReportServiceImpl extends AbstractServiceImpl<GradingReport,
return getGradingBasis(project); return getGradingBasis(project);
} }
@Override
public Instant getDateSentToExaminer(Project project) {
return getSupervisorGradingReports(project)
.stream()
.map(SupervisorGradingReport::getDateSubmittedToExaminer)
.filter(Objects::nonNull)
.max(Comparator.naturalOrder())
.orElse(null);
}
private GradingBasis.Assessment toAssessment( private GradingBasis.Assessment toAssessment(
Language language, Language language,
GradingCriterion gc) { GradingCriterion gc) {

@ -12,6 +12,9 @@
transaction-type="RESOURCE_LOCAL"> transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider> <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<non-jta-data-source>java:/comp/env/jdbc/sciproDS</non-jta-data-source> <non-jta-data-source>java:/comp/env/jdbc/sciproDS</non-jta-data-source>
<properties>
<property name="hibernate.show_sql" value="false"/>
</properties>
</persistence-unit> </persistence-unit>
<!-- A JPA Persistence Unit used for tests --> <!-- A JPA Persistence Unit used for tests -->

@ -0,0 +1,2 @@
alter table GradingReport
add date_submitted_to_examiner datetime null;

@ -11,8 +11,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
import se.su.dsv.scipro.forum.dataobjects.ForumPost; import se.su.dsv.scipro.forum.dataobjects.ForumPost;
import se.su.dsv.scipro.forum.dataobjects.ForumPostReadState; import se.su.dsv.scipro.forum.dataobjects.ForumPostReadState;
import se.su.dsv.scipro.forum.dataobjects.ForumThread; import se.su.dsv.scipro.forum.dataobjects.ForumThread;
import se.su.dsv.scipro.forummail.ForumMailSettingsService;
import se.su.dsv.scipro.mail.MailEventService;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.test.ForumBuilder; import se.su.dsv.scipro.test.ForumBuilder;
import se.su.dsv.scipro.test.UserBuilder; import se.su.dsv.scipro.test.UserBuilder;
@ -35,10 +33,6 @@ public class BasicForumServiceImplTest {
@Mock @Mock
private ForumPostRepository postRepository; private ForumPostRepository postRepository;
@Mock @Mock
private MailEventService mailEventService;
@Mock
private ForumMailSettingsService mailSettingsService;
@Mock
private EventBus eventBus; private EventBus eventBus;
@InjectMocks @InjectMocks
private BasicForumServiceImpl basicForumService; private BasicForumServiceImpl basicForumService;
@ -93,23 +87,20 @@ public class BasicForumServiceImplTest {
} }
@Test @Test
public void testMarkThreadRead() { public void testMarkThreadReadPostsEvent() {
when(readStateRepository.find(any(User.class), any(ForumPost.class))).thenReturn(new ForumPostReadState());
when(readStateRepository.save(isA(ForumPostReadState.class))).thenAnswer(AdditionalAnswers.returnsFirstArg());
User user = new User(); User user = new User();
ForumPost post = new ForumPost(); ForumPost post = new ForumPost();
post.setContent("post 1");
ForumPost post2 = new ForumPost(); ForumPost post2 = new ForumPost();
post2.setContent("post 2");
ForumThread forumThread = new ForumThread(); ForumThread forumThread = new ForumThread();
forumThread.addPost(post); forumThread.addPost(post);
forumThread.addPost(post2); forumThread.addPost(post2);
basicForumService.setThreadRead(user, forumThread, true); basicForumService.setThreadRead(user, forumThread, true);
ArgumentCaptor<ForumPostReadState> captor = ArgumentCaptor.forClass(ForumPostReadState.class); verify(eventBus).post(new ForumPostReadEvent(post, user));
verify(readStateRepository, times(2)).save(captor.capture()); verify(eventBus).post(new ForumPostReadEvent(post2, user));
assertTrue(captor.getValue().isRead(), "Did not save correct read state");
} }
@Test @Test
@ -181,8 +172,8 @@ public class BasicForumServiceImplTest {
verify(eventBus).post(captor.capture()); verify(eventBus).post(captor.capture());
ForumPostReadEvent event = captor.getValue(); ForumPostReadEvent event = captor.getValue();
assertEquals(event.getPost(), post); assertEquals(event.post(), post);
assertEquals(event.getUser(), user); assertEquals(event.user(), user);
} }
@Test @Test

@ -204,7 +204,12 @@ public class IdeaServiceImplTest {
when(applicationPeriodService.getTypesForStudent(applicationPeriod, student)) when(applicationPeriodService.getTypesForStudent(applicationPeriod, student))
.thenReturn(List.of(bachelor)); .thenReturn(List.of(bachelor));
assertPair(true, SELECTED_IDEA + idea.getTitle(), ideaService.validateStudentAcceptance(idea, student, coAuthor, applicationPeriod)); Pair<Boolean, String> acceptance = ideaService.validateStudentAcceptance(
idea,
student,
coAuthor,
applicationPeriod);
assertTrue(acceptance.getHead());
} }
@Test @Test

@ -51,4 +51,18 @@
</notes> </notes>
<cve>CVE-2023-35116</cve> <cve>CVE-2023-35116</cve>
</suppress> </suppress>
<suppress>
<notes>
This is a complete nonsense vulnerability. Some automated tool has
gone completely bananas.
</notes>
<cve>CVE-2024-22949</cve>
</suppress>
<suppress>
<notes>
This is a complete nonsense vulnerability. Some automated tool has
gone completely bananas.
</notes>
<cve>CVE-2023-52070</cve>
</suppress>
</suppressions> </suppressions>

@ -64,6 +64,11 @@
<groupId>com.lowagie</groupId> <groupId>com.lowagie</groupId>
<artifactId>itext</artifactId> <artifactId>itext</artifactId>
</exclusion> </exclusion>
<exclusion>
<!-- until a new version containing https://github.com/wicketstuff/core/pull/873 is released -->
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
</exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<dependency> <dependency>

@ -5,6 +5,7 @@ import org.apache.wicket.*;
import org.apache.wicket.authorization.strategies.CompoundAuthorizationStrategy; import org.apache.wicket.authorization.strategies.CompoundAuthorizationStrategy;
import org.apache.wicket.csp.CSPDirective; import org.apache.wicket.csp.CSPDirective;
import org.apache.wicket.csp.CSPDirectiveSrcValue; import org.apache.wicket.csp.CSPDirectiveSrcValue;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.request.Request; import org.apache.wicket.request.Request;
import org.apache.wicket.request.Response; import org.apache.wicket.request.Response;
import org.apache.wicket.resource.JQueryResourceReference; import org.apache.wicket.resource.JQueryResourceReference;
@ -24,6 +25,7 @@ import se.su.dsv.scipro.checklists.AdminChecklistPage;
import se.su.dsv.scipro.checklists.AdminEditChecklistTemplatePage; import se.su.dsv.scipro.checklists.AdminEditChecklistTemplatePage;
import se.su.dsv.scipro.checklists.ProjectViewChecklistPage; import se.su.dsv.scipro.checklists.ProjectViewChecklistPage;
import se.su.dsv.scipro.checklists.SupervisorViewChecklistPage; import se.su.dsv.scipro.checklists.SupervisorViewChecklistPage;
import se.su.dsv.scipro.components.DisableSubmitButtonsOnSubmit;
import se.su.dsv.scipro.examiner.pages.ExaminerStartPage; import se.su.dsv.scipro.examiner.pages.ExaminerStartPage;
import se.su.dsv.scipro.finalseminar.*; import se.su.dsv.scipro.finalseminar.*;
import se.su.dsv.scipro.finalthesis.SupervisorFinalThesisListingPage; import se.su.dsv.scipro.finalthesis.SupervisorFinalThesisListingPage;
@ -80,6 +82,7 @@ import se.su.dsv.scipro.util.AdditionalExceptionLogger;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
public class SciProApplication extends LifecycleManagedWebApplication { public class SciProApplication extends LifecycleManagedWebApplication {
@ -106,6 +109,12 @@ public class SciProApplication extends LifecycleManagedWebApplication {
return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
} }
}); });
converterLocator.set(ZonedDateTime.class, new LocalDateTimeConverter() {
@Override
protected DateTimeFormatter getDateTimeFormatter() {
return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
}
});
return converterLocator; return converterLocator;
} }
@ -150,6 +159,12 @@ public class SciProApplication extends LifecycleManagedWebApplication {
.add(CSPDirective.IMG_SRC, "data:"); .add(CSPDirective.IMG_SRC, "data:");
WicketWebjars.install(this); WicketWebjars.install(this);
getComponentInstantiationListeners().add(component -> {
if (component instanceof Form) {
component.add(new DisableSubmitButtonsOnSubmit());
}
});
} }
private void mountForumPage() { private void mountForumPage() {

@ -22,8 +22,10 @@
<dt>Research area</dt> <dt>Research area</dt>
<dd wicket:id="research_area"></dd> <dd wicket:id="research_area"></dd>
<dt>Language</dt> <wicket:enclosure>
<dd wicket:id="language"></dd> <dt>Language</dt>
<dd wicket:id="language"></dd>
</wicket:enclosure>
<wicket:enclosure> <wicket:enclosure>
<dt>Reviewer requested at</dt> <dt>Reviewer requested at</dt>

@ -84,7 +84,13 @@ public class AdminAssignReviewerPage extends AbstractAdminProjectPage {
add(new Label("title", projectModel.map(Project::getTitle))); add(new Label("title", projectModel.map(Project::getTitle)));
add(new Label("research_area", projectModel.map(Project::getResearchArea).map(ResearchArea::getTitle))); add(new Label("research_area", projectModel.map(Project::getResearchArea).map(ResearchArea::getTitle)));
add(new UserLinkPanel("supervisor", projectModel.map(Project::getHeadSupervisor))); add(new UserLinkPanel("supervisor", projectModel.map(Project::getHeadSupervisor)));
add(new EnumLabel<>("language", projectModel.map(Project::getLanguage))); add(new EnumLabel<>("language", projectModel.map(Project::getLanguage)){
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(getDefaultModelObject() != null);
}
});
add(new ViewAttachmentPanel("rough_draft", roughDraftApproval.map(RoughDraftApproval::getCurrentThesis).map(FileReference::getFileDescription))); add(new ViewAttachmentPanel("rough_draft", roughDraftApproval.map(RoughDraftApproval::getCurrentThesis).map(FileReference::getFileDescription)));
add(new DateLabel("requested_at", roughDraftApproval.map(RoughDraftApproval::getCurrentDecision).map(Decision::getRequested), DateStyle.DATETIME) { add(new DateLabel("requested_at", roughDraftApproval.map(RoughDraftApproval::getCurrentDecision).map(Decision::getRequested), DateStyle.DATETIME) {
@Override @Override

@ -0,0 +1,30 @@
package se.su.dsv.scipro.components;
import org.apache.wicket.Component;
import org.apache.wicket.behavior.Behavior;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.OnEventHeaderItem;
import org.apache.wicket.markup.html.form.Form;
/**
* Disables all elements with {@code [type=submit]}
*/
public class DisableSubmitButtonsOnSubmit extends Behavior {
@Override
public void bind(Component component) {
super.bind(component);
if (!(component instanceof Form<?>)) {
throw new RuntimeException("Can only be used on Form components");
}
}
@Override
public void renderHead(Component component, IHeaderResponse response) {
super.renderHead(component, response);
final String javaScript = "const submitButtons = event.target.querySelectorAll(\"[type=submit]\");\n" +
"for (const button of submitButtons) {\n" +
" button.disabled = true;\n" +
"}\n";
response.render(OnEventHeaderItem.forComponent(component, "submit", javaScript));
}
}

@ -4,7 +4,11 @@
<wicket:panel> <wicket:panel>
<strong>Status:</strong> <span wicket:id="status"></span> <strong>Status:</strong> <span wicket:id="status"></span>
<br> <br>
<span wicket:id="approvedDate"></span><br> Approved by supervisor: <span wicket:id="approvedDate"></span><br>
<wicket:enclosure>
Submitted to examiner: <span wicket:id="submittedToExaminerTimestamp"></span><br>
</wicket:enclosure>
<br>
</wicket:panel> </wicket:panel>
</body> </body>
</html> </html>

@ -3,6 +3,7 @@ package se.su.dsv.scipro.finalthesis;
import org.apache.wicket.feedback.FencedFeedbackPanel; import org.apache.wicket.feedback.FencedFeedbackPanel;
import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.EnumLabel; import org.apache.wicket.markup.html.basic.EnumLabel;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.DropDownChoice; import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.EnumChoiceRenderer; import org.apache.wicket.markup.html.form.EnumChoiceRenderer;
import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.form.Form;
@ -19,6 +20,7 @@ import se.su.dsv.scipro.finalseminar.FinalSeminarService;
import se.su.dsv.scipro.forum.pages.ProjectForumBasePage; import se.su.dsv.scipro.forum.pages.ProjectForumBasePage;
import se.su.dsv.scipro.project.Project; import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.reflection.ReflectionService; import se.su.dsv.scipro.reflection.ReflectionService;
import se.su.dsv.scipro.report.GradingReportService;
import se.su.dsv.scipro.security.auth.ProjectModuleComponent; import se.su.dsv.scipro.security.auth.ProjectModuleComponent;
import se.su.dsv.scipro.session.SciProSession; import se.su.dsv.scipro.session.SciProSession;
import se.su.dsv.scipro.system.ProjectModule; import se.su.dsv.scipro.system.ProjectModule;
@ -26,6 +28,8 @@ import se.su.dsv.scipro.util.PageParameterKeys;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List; import java.util.List;
import static se.su.dsv.scipro.finalthesis.FinalThesis.Status; import static se.su.dsv.scipro.finalthesis.FinalThesis.Status;
@ -40,6 +44,7 @@ public class FinalThesisPanel extends GenericPanel<Project> {
public static final String APPROVED_PANEL = "approvedPanel"; public static final String APPROVED_PANEL = "approvedPanel";
public static final String APPROVED_DATE = "approvedDate"; public static final String APPROVED_DATE = "approvedDate";
public static final String NO_DECISION_PANEL = "noDecisionPanel"; public static final String NO_DECISION_PANEL = "noDecisionPanel";
public static final String SUBMITTED_TO_EXAMINER_TIMESTAMP = "submittedToExaminerTimestamp";
@Inject @Inject
private FinalThesisService finalThesisService; private FinalThesisService finalThesisService;
@ -49,6 +54,8 @@ public class FinalThesisPanel extends GenericPanel<Project> {
private PublishingConsentService publishingConsentService; private PublishingConsentService publishingConsentService;
@Inject @Inject
private ReflectionService reflectionService; private ReflectionService reflectionService;
@Inject
private GradingReportService gradingReportService;
public FinalThesisPanel(String id, IModel<Project> project) { public FinalThesisPanel(String id, IModel<Project> project) {
super(id, project); super(id, project);
@ -96,9 +103,17 @@ public class FinalThesisPanel extends GenericPanel<Project> {
private class ApprovedPanel extends Panel { private class ApprovedPanel extends Panel {
public ApprovedPanel(String id) { public ApprovedPanel(String id) {
super(id); super(id);
add(new EnumLabel<>("status", getModel().map(Project::getProjectStatus))); add(new EnumLabel<>("status", getModel().map(Project::getProjectStatus)));
add(new DateLabel(APPROVED_DATE, getFinalThesis().map(FinalThesis::getDateApproved))); add(new DateLabel(APPROVED_DATE, getFinalThesis().map(FinalThesis::getDateApproved)));
IModel<ZonedDateTime> submittedToExaminerTimestamp = LoadableDetachableModel.of(() -> gradingReportService.getDateSentToExaminer(getModelObject()))
.map(instant -> instant.atZone(ZoneId.systemDefault()));
add(new Label(SUBMITTED_TO_EXAMINER_TIMESTAMP, submittedToExaminerTimestamp) {
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(submittedToExaminerTimestamp.getObject() != null);
}
});
} }
@Override @Override

@ -246,7 +246,9 @@ public class CriteriaPanel extends GenericPanel<SupervisorGradingReport> {
@Override @Override
public void setObject(GradingCriterionPoint object) { public void setObject(GradingCriterionPoint object) {
criterionIModel.getObject().setPoints(object.getPoint()); if (object != null) {
criterionIModel.getObject().setPoints(object.getPoint());
}
} }
} }

@ -1,4 +1,4 @@
save = Save save = Save
overall_motivation = Overall motivation overall_motivation = Overall motivation
grading_basis_updated = Assessment saved at ${} grading_basis_updated = Assessment saved at ${}
rejection_comment_feedback.Required = Rejection commend feedback must be provided. rejection_comment_feedback.Required = Rejection comment feedback must be provided.

@ -36,6 +36,7 @@
<ul> <ul>
<li wicket:id="status_final_thesis"></li> <li wicket:id="status_final_thesis"></li>
<li wicket:id="status_plagiarism"></li> <li wicket:id="status_plagiarism"></li>
<li wicket:id="status_publication_metadata"></li>
<li> <li>
<div wicket:id="status_grading_basis">></div> <div wicket:id="status_grading_basis">></div>
<ul wicket:id="grading_basis_missing"> <ul wicket:id="grading_basis_missing">

@ -33,8 +33,10 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.function.Predicate; import java.util.function.Predicate;
public class IndividualAuthorAssessment extends GenericPanel<User> { public class IndividualAuthorAssessmentPanel extends GenericPanel<User> {
@Inject
private NationalSubjectCategoryService nationalSubjectCategoryService;
@Inject @Inject
private GradingReportService gradingReportService; private GradingReportService gradingReportService;
@Inject @Inject
@ -43,10 +45,12 @@ public class IndividualAuthorAssessment extends GenericPanel<User> {
private FinalThesisService finalThesisService; private FinalThesisService finalThesisService;
@Inject @Inject
private FinalSeminarService finalSeminarService; private FinalSeminarService finalSeminarService;
@Inject
private PublicationMetadataService publicationMetadataService;
private final IModel<Project> projectModel; private final IModel<Project> projectModel;
public IndividualAuthorAssessment(String id, IModel<Project> projectModel, IModel<User> authorModel) { public IndividualAuthorAssessmentPanel(String id, IModel<Project> projectModel, IModel<User> authorModel) {
super(id, authorModel); super(id, authorModel);
this.projectModel = projectModel; this.projectModel = projectModel;
@ -73,6 +77,10 @@ public class IndividualAuthorAssessment extends GenericPanel<User> {
redGreen("status_plagiarism", hasSubmittedPlagiarismAnalysis, redGreen("status_plagiarism", hasSubmittedPlagiarismAnalysis,
"must_perform_plagiarism_check", "must_perform_plagiarism_check",
"plagiarism_check_performed"); "plagiarism_check_performed");
IModel<Boolean> hasSuppliedPublicationMetadata = Model.of(publicationMetadataService.hasSuppliedPublicationMetadata(projectModel.getObject(), nationalSubjectCategoryService.listCategories().isEmpty()));
redGreen("status_publication_metadata", hasSuppliedPublicationMetadata,
"must_supply_publication_metadata",
"publication_metadata_supplied");
IModel<Boolean> hasFilledInGradingBasis = gradingReport.map(this::gradingBasisDone); IModel<Boolean> hasFilledInGradingBasis = gradingReport.map(this::gradingBasisDone);
redGreen("status_grading_basis", hasFilledInGradingBasis, redGreen("status_grading_basis", hasFilledInGradingBasis,
"grading_basis_must_meet_minimum_requirements", "grading_basis_must_meet_minimum_requirements",
@ -169,6 +177,7 @@ public class IndividualAuthorAssessment extends GenericPanel<User> {
super.onConfigure(); super.onConfigure();
setVisible(hasApprovedFinalThesis.getObject() setVisible(hasApprovedFinalThesis.getObject()
&& hasSubmittedPlagiarismAnalysis.getObject() && hasSubmittedPlagiarismAnalysis.getObject()
&& hasSuppliedPublicationMetadata.getObject()
&& hasFilledInGradingBasis.getObject() && hasFilledInGradingBasis.getObject()
&& hasFilledInIndividualAssessment.getObject()); && hasFilledInIndividualAssessment.getObject());
} }

@ -4,6 +4,8 @@ must_approve_final_thesis = You must approve the final thesis.
final_thesis_approved = Final thesis approved. final_thesis_approved = Final thesis approved.
must_perform_plagiarism_check = You have to check the text matching report and perform a plagiarism analysis. must_perform_plagiarism_check = You have to check the text matching report and perform a plagiarism analysis.
plagiarism_check_performed = Plagiarism analysis submitted. plagiarism_check_performed = Plagiarism analysis submitted.
must_supply_publication_metadata = You must supply publication metadata.
publication_metadata_supplied = Publication metadata supplied.
grading_basis_must_meet_minimum_requirements = General criteria not met. grading_basis_must_meet_minimum_requirements = General criteria not met.
grading_basis_minimum_requirements_met = General criteria met. grading_basis_minimum_requirements_met = General criteria met.
individual_assessment_must_meet_minimum_requirements = Not all individual criteria are met. individual_assessment_must_meet_minimum_requirements = Not all individual criteria are met.

@ -3,12 +3,12 @@
<body> <body>
<wicket:panel> <wicket:panel>
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="abstract_en">Abstract (English)</label> <label class="form-label" for="abstract_en">Abstract (English) (required)</label>
<textarea class="form-control" id="abstract_en" wicket:id="abstract_en"></textarea> <textarea class="form-control" id="abstract_en" wicket:id="abstract_en"></textarea>
</div> </div>
<wicket:enclosure> <wicket:enclosure>
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="abstract_sv">Abstract (Swedish)</label> <label class="form-label" for="abstract_sv">Abstract (Swedish) (required)</label>
<textarea class="form-control" id="abstract_sv" wicket:id="abstract_sv"></textarea> <textarea class="form-control" id="abstract_sv" wicket:id="abstract_sv"></textarea>
</div> </div>
</wicket:enclosure> </wicket:enclosure>
@ -22,11 +22,13 @@
<input class="form-control" id="keywords_sv" wicket:id="keywords_sv"> <input class="form-control" id="keywords_sv" wicket:id="keywords_sv">
</div> </div>
</wicket:enclosure> </wicket:enclosure>
<div class="mb-3"> <wicket:enclosure>
<label class="form-label" for="national_subject_category">National subject category</label> <div class="mb-3">
<select class="form-select" id="national_subject_category" wicket:id="national_subject_category"> <label class="form-label" for="national_subject_category">National subject category (required)</label>
</select> <select class="form-select" id="national_subject_category" wicket:id="national_subject_category">
</div> </select>
</div>
</wicket:enclosure>
</wicket:panel> </wicket:panel>
</body> </body>
</html> </html>

@ -45,6 +45,7 @@ public class PublicationMetadataFormComponentPanel extends GenericPanel<Publicat
.ifPresent(nationalSubjectCategoryChoice::setDefaultModelObject); .ifPresent(nationalSubjectCategoryChoice::setDefaultModelObject);
} }
nationalSubjectCategoryChoice.setNullValid(true); nationalSubjectCategoryChoice.setNullValid(true);
nationalSubjectCategoryChoice.setVisible(!availableCategories.getObject().isEmpty());
add(nationalSubjectCategoryChoice); add(nationalSubjectCategoryChoice);
} }

@ -2,9 +2,20 @@
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org" lang="en"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org" lang="en">
<body> <body>
<wicket:panel> <wicket:panel>
<wicket:enclosure child="send"> <wicket:enclosure child="form">
<div wicket:id="feedback"></div> <div wicket:id="feedback"></div>
<button type="button" class="btn btn-success scrollSneak" wicket:id="send">Send thesis for examination</button> <form wicket:id="form">
<div class="mb-3">
<label for="examinationDate" class="form-label">Examination date</label>
<input type="text" class="form-control" id="examinationDate" wicket:id="examinationDate" required>
<small class="form-text">
The examination date is suggested based on the last student activity (submitted final thesis,
opposition, or active participation). You should only change this date if the last activity was at a
different date.
</small>
</div>
<button type="submit" class="btn btn-success scrollSneak" wicket:id="send">Send thesis for examination</button>
</form>
</wicket:enclosure> </wicket:enclosure>
<p class="card-text" wicket:id="already_sent"> <p class="card-text" wicket:id="already_sent">
<span class="fa fa-check text-success"></span> <span class="fa fa-check text-success"></span>

@ -2,10 +2,13 @@ package se.su.dsv.scipro.grading;
import org.apache.wicket.feedback.FencedFeedbackPanel; import org.apache.wicket.feedback.FencedFeedbackPanel;
import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.panel.GenericPanel; import org.apache.wicket.markup.html.panel.GenericPanel;
import org.apache.wicket.model.IModel; import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.LoadableDetachableModel;
import se.su.dsv.scipro.components.ConfirmationLink; import org.apache.wicket.model.Model;
import se.su.dsv.scipro.components.BootstrapDatePicker;
import se.su.dsv.scipro.daisyExternal.http.DaisyAPI; import se.su.dsv.scipro.daisyExternal.http.DaisyAPI;
import se.su.dsv.scipro.file.FileDescription; import se.su.dsv.scipro.file.FileDescription;
import se.su.dsv.scipro.file.FileService; import se.su.dsv.scipro.file.FileService;
@ -33,6 +36,7 @@ import se.su.dsv.scipro.system.Language;
import se.su.dsv.scipro.system.ResearchArea; import se.su.dsv.scipro.system.ResearchArea;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.util.Either; import se.su.dsv.scipro.util.Either;
import se.su.dsv.scipro.util.JavascriptEventConfirmation;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import java.time.LocalDate; import java.time.LocalDate;
@ -75,10 +79,16 @@ public class SendToExaminer extends GenericPanel<Project> {
super(id, projectModel); super(id, projectModel);
needsSending = LoadableDetachableModel.of(() -> hasGradedExaminationWithoutSuggestion(authorModel.getObject())); needsSending = LoadableDetachableModel.of(() -> hasGradedExaminationWithoutSuggestion(authorModel.getObject()));
add(new ConfirmationLink<>("send", authorModel, confirmationMessage) {
IModel<LocalDate> examinationDate = new Model<>();
FinalThesis finalThesis = finalThesisService.findByProject(projectModel.getObject());
examinationDate.setObject(getExaminationDate(authorModel.getObject(), projectModel.getObject(), finalThesis));
Form<Void> form = new Form<>("form") {
@Override @Override
public void onClick() { protected void onSubmit() {
sendToExaminer(getModelObject()); super.onSubmit();
sendToExaminer(authorModel.getObject(), examinationDate.getObject());
} }
@Override @Override
@ -86,7 +96,20 @@ public class SendToExaminer extends GenericPanel<Project> {
super.onConfigure(); super.onConfigure();
setVisible(needsSending.getObject()); setVisible(needsSending.getObject());
} }
}); };
add(form);
WebMarkupContainer sendButton = new WebMarkupContainer("send");
if (confirmationMessage.getObject() != null) {
sendButton.add(new JavascriptEventConfirmation("click", confirmationMessage));
}
form.add(sendButton);
TextField<LocalDate> examinationDateField = new TextField<>("examinationDate", examinationDate, LocalDate.class);
examinationDateField.setRequired(true);
examinationDateField.add(new BootstrapDatePicker());
form.add(examinationDateField);
add(new WebMarkupContainer("already_sent") { add(new WebMarkupContainer("already_sent") {
@Override @Override
protected void onConfigure() { protected void onConfigure() {
@ -101,6 +124,10 @@ public class SendToExaminer extends GenericPanel<Project> {
String token = getSession().getMetaData(OAuth.TOKEN); String token = getSession().getMetaData(OAuth.TOKEN);
Project project = getModelObject(); Project project = getModelObject();
List<Examination> examinations = gradingService.getExaminations(token, project.getIdentifier(), author.getIdentifier()); List<Examination> examinations = gradingService.getExaminations(token, project.getIdentifier(), author.getIdentifier());
if (examinations == null) {
// if we can't tell assume it is not sent
return true;
}
for (Examination examination : examinations) { for (Examination examination : examinations) {
if (examination.hasManyPassingGrades()) { if (examination.hasManyPassingGrades()) {
Either<GetGradeError, Optional<Result>> result = gradingService.getResult(token, project.getIdentifier(), author.getIdentifier(), examination.id()); Either<GetGradeError, Optional<Result>> result = gradingService.getResult(token, project.getIdentifier(), author.getIdentifier(), examination.id());
@ -112,7 +139,7 @@ public class SendToExaminer extends GenericPanel<Project> {
return false; return false;
} }
private void sendToExaminer(User author) { private void sendToExaminer(User author, LocalDate examinationDate) {
checkStepsMissing(); checkStepsMissing();
if (hasErrorMessage()) { if (hasErrorMessage()) {
// some steps have not been completed // some steps have not been completed
@ -125,6 +152,10 @@ public class SendToExaminer extends GenericPanel<Project> {
return; return;
} }
List<Examination> examinations = gradingService.getExaminations(token, project.getIdentifier(), author.getIdentifier()); List<Examination> examinations = gradingService.getExaminations(token, project.getIdentifier(), author.getIdentifier());
if (examinations == null) {
getSession().error("Failed to get the examination setup for " + author.getFullName());
return;
}
List<Examination> gradedExaminations = examinations List<Examination> gradedExaminations = examinations
.stream() .stream()
.filter(Examination::hasManyPassingGrades) .filter(Examination::hasManyPassingGrades)
@ -138,7 +169,7 @@ public class SendToExaminer extends GenericPanel<Project> {
} else if (gradedExaminations.isEmpty()) { } else if (gradedExaminations.isEmpty()) {
getSession().info("Nothing to report on " + author.getFullName()); getSession().info("Nothing to report on " + author.getFullName());
} else { } else {
sendSuggestion(project, author, gradedExaminations.get(0)); sendSuggestion(project, author, gradedExaminations.get(0), examinationDate);
} }
needsSending.detach(); needsSending.detach();
} }
@ -176,7 +207,7 @@ public class SendToExaminer extends GenericPanel<Project> {
return missing; return missing;
} }
private void sendSuggestion(Project project, User author, Examination examination) { private void sendSuggestion(Project project, User author, Examination examination, LocalDate examinationDate) {
String token = getSession().getMetaData(OAuth.TOKEN); String token = getSession().getMetaData(OAuth.TOKEN);
Either<GetGradeError, Optional<Result>> currentResult Either<GetGradeError, Optional<Result>> currentResult
@ -197,7 +228,6 @@ public class SendToExaminer extends GenericPanel<Project> {
GradeCalculator gradeCalculator = gradeCalculatorService.getSupervisorCalculator(project); GradeCalculator gradeCalculator = gradeCalculatorService.getSupervisorCalculator(project);
SupervisorGradingReport supervisorGradingReport = gradingReportService.getSupervisorGradingReport(getModelObject(), author); SupervisorGradingReport supervisorGradingReport = gradingReportService.getSupervisorGradingReport(getModelObject(), author);
GradingReport.Grade grade = gradeCalculator.getGrade(supervisorGradingReport); GradingReport.Grade grade = gradeCalculator.getGrade(supervisorGradingReport);
LocalDate examinationDate = getExaminationDate(author, project, finalThesis);
Either<ReportGradeError, Void> reported = Either<ReportGradeError, Void> reported =
gradingService.reportGrade( gradingService.reportGrade(
token, token,

@ -56,7 +56,8 @@ public class SupervisorGradingPage extends AbstractSupervisorProjectDetailsPage
@Override @Override
protected void populateItem(final ListItem<User> item) { protected void populateItem(final ListItem<User> item) {
item.add(new UserLabel("authorName", item.getModel())); item.add(new UserLabel("authorName", item.getModel()));
final IModel<List<Examination>> examinations = SupervisorGradingPage.this.getExaminations(item.getModel()); final IModel<List<Examination>> examinations = SupervisorGradingPage.this.getExaminations(item.getModel())
.orElseGet(Collections::emptyList);
final IModel<List<Examination>> nonGradedExaminations = getSpecificExaminations(examinations, false); final IModel<List<Examination>> nonGradedExaminations = getSpecificExaminations(examinations, false);
item.add(new NonGradedPanel( item.add(new NonGradedPanel(

@ -46,6 +46,8 @@ import java.util.Set;
@ProjectModuleComponent(ProjectModule.GRADING) @ProjectModuleComponent(ProjectModule.GRADING)
public class SupervisorGradingReportPage extends AbstractSupervisorProjectDetailsPage { public class SupervisorGradingReportPage extends AbstractSupervisorProjectDetailsPage {
@Inject
private NationalSubjectCategoryService nationalSubjectCategoryService;
@Inject @Inject
private GeneralSystemSettingsService generalSystemSettingsService; private GeneralSystemSettingsService generalSystemSettingsService;
@Inject @Inject
@ -76,11 +78,7 @@ public class SupervisorGradingReportPage extends AbstractSupervisorProjectDetail
.isPresent(); .isPresent();
add(newGreenHighlight("step_plagiarism", hasCheckedForPlagiarism, new ResourceModel("step_plagiarism"))); add(newGreenHighlight("step_plagiarism", hasCheckedForPlagiarism, new ResourceModel("step_plagiarism")));
IModel<Boolean> hasProvidedPublicationMetadata = IModel<Boolean> hasProvidedPublicationMetadata = Model.of(publicationMetadataService.hasSuppliedPublicationMetadata(projectModel.getObject(), nationalSubjectCategoryService.listCategories().isEmpty()));
projectModel.map(publicationMetadataService::getByProject)
.filter(metadata -> notBlank(metadata.getAbstractEnglish()) || notBlank(metadata.getAbstractSwedish()))
.filter(metadata -> notBlank(metadata.getKeywordsEnglish()) || notBlank(metadata.getKeywordsSwedish()))
.isPresent();
add(newGreenHighlight("step_publication_metadata", hasProvidedPublicationMetadata, new ResourceModel("step_publication_metadata"))); add(newGreenHighlight("step_publication_metadata", hasProvidedPublicationMetadata, new ResourceModel("step_publication_metadata")));
IModel<List<SupervisorGradingReport>> gradingReports = LoadableDetachableModel.of(() -> IModel<List<SupervisorGradingReport>> gradingReports = LoadableDetachableModel.of(() ->
@ -131,7 +129,7 @@ public class SupervisorGradingReportPage extends AbstractSupervisorProjectDetail
.map(author -> new DetachableServiceModel<>(userService, author)) .map(author -> new DetachableServiceModel<>(userService, author))
.map(authorModel -> createTab( .map(authorModel -> createTab(
authorModel.map(User::getFullName), authorModel.map(User::getFullName),
panelId -> new IndividualAuthorAssessment(panelId, projectModel, authorModel))) panelId -> new IndividualAuthorAssessmentPanel(panelId, projectModel, authorModel)))
.toList(); .toList();
tabs.addAll(authorTabs); tabs.addAll(authorTabs);
@ -151,10 +149,6 @@ public class SupervisorGradingReportPage extends AbstractSupervisorProjectDetail
add(new ExaminerTimelinePanel("examiner_timeline", projectModel)); add(new ExaminerTimelinePanel("examiner_timeline", projectModel));
} }
private boolean notBlank(String s) {
return s != null && !s.isBlank();
}
private Component newGreenHighlight(String id, IModel<Boolean> completed, IModel<String> text) { private Component newGreenHighlight(String id, IModel<Boolean> completed, IModel<String> text) {
return new RedGreenLabel(id, completed, text, text); return new RedGreenLabel(id, completed, text, text);
} }
@ -176,6 +170,10 @@ public class SupervisorGradingReportPage extends AbstractSupervisorProjectDetail
String token = getSession().getMetaData(OAuth.TOKEN); String token = getSession().getMetaData(OAuth.TOKEN);
Project project = projectModel.getObject(); Project project = projectModel.getObject();
List<Examination> examinations = gradingService.getExaminations(token, project.getIdentifier(), author.getIdentifier()); List<Examination> examinations = gradingService.getExaminations(token, project.getIdentifier(), author.getIdentifier());
if (examinations == null) {
// if grading service is down, assume not sent
return false;
}
for (Examination examination : examinations) { for (Examination examination : examinations) {
if (examination.hasManyPassingGrades()) { if (examination.hasManyPassingGrades()) {
Either<GetGradeError, Optional<Result>> result = gradingService.getResult(token, project.getIdentifier(), author.getIdentifier(), examination.id()); Either<GetGradeError, Optional<Result>> result = gradingService.getResult(token, project.getIdentifier(), author.getIdentifier(), examination.id());

@ -1,9 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org"> <html xmlns:wicket="http://wicket.apache.org">
<body> <body>
<wicket:extend> <wicket:extend>
<div class="row"></div> <div class="row">
<div wicket:id="submissionPanel"></div> <div class="col-lg-8 col-xl-6" wicket:id="submissionPanel"></div>
</div>
</wicket:extend> </wicket:extend>
</body> </body>
</html> </html>

@ -4,6 +4,7 @@ import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior; import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior;
import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.*; import org.apache.wicket.markup.html.form.*;
import org.apache.wicket.markup.html.link.BookmarkablePageLink; import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.link.ExternalLink; import org.apache.wicket.markup.html.link.ExternalLink;
@ -116,7 +117,7 @@ public class ProjectIdeaSubmissionPanel extends GenericPanel<Idea> {
addResearchAreaAndKeywordsSelection(); addResearchAreaAndKeywordsSelection();
addContract(); addContract();
addDeleteButton(); addDeleteButton();
add(new Button("save")); add(new Label("save", isNewIdea ? new ResourceModel("save") : new ResourceModel("update")));
} }
@Override @Override
@ -321,8 +322,13 @@ public class ProjectIdeaSubmissionPanel extends GenericPanel<Idea> {
@Override @Override
protected void onSubmit() { protected void onSubmit() {
ideaService.saveStudentIdea(getModelObject(), creator, programDropDownChoice.getModelObject(), new HashSet<>(coAuthorChoice.getModelObject()), Idea idea = ideaService.saveStudentIdea(getModelObject(), creator, programDropDownChoice.getModelObject(), new HashSet<>(coAuthorChoice.getModelObject()),
new ArrayList<>(keywords), isNewIdea); new ArrayList<>(keywords), isNewIdea);
if (isNewIdea) {
getSession().success(getString("ideaSubmitted", Model.of(idea)));
} else {
getSession().success(getString("ideaUpdated", Model.of(idea)));
}
setResponsePage(ProjectIdeaStartPage.class); setResponsePage(ProjectIdeaStartPage.class);
} }

@ -27,6 +27,10 @@ too.many.authors= Too many authors for a ${name}-idea. May not be more than ${ma
too.few.authors= Too few authors for a ${name}-idea. Must be at least ${minAuthors} including yourself. too.few.authors= Too few authors for a ${name}-idea. Must be at least ${minAuthors} including yourself.
keywordError= You need to select between 1 and 5 keywords. keywordError= You need to select between 1 and 5 keywords.
submissionFailed= Idea could not be submitted. submissionFailed= Idea could not be submitted.
ideaSubmitted= You have successfully submitted your ${projectType.name} student idea, for the application period \
${applicationPeriod.name}.
ideaUpdated= You have successfully updated your ${projectType.name} student idea, in the application period \
${applicationPeriod.name}.
titleInfo= The idea title will become the project title once the idea has been matched to a supervisor and the course started. The title can then only be changed by the supervisor. titleInfo= The idea title will become the project title once the idea has been matched to a supervisor and the course started. The title can then only be changed by the supervisor.
researchAreaInfo= The idea should be connected to a research area. researchAreaInfo= The idea should be connected to a research area.
languageInfo= The language the thesis will be written in. This will affect many areas of the thesis writing process \ languageInfo= The language the thesis will be written in. This will affect many areas of the thesis writing process \
@ -35,3 +39,5 @@ programInfo= Select the program within the context of which you are doing this i
programDropDown.nullValid=Not within a program programDropDown.nullValid=Not within a program
you.already.have.an.active.project.on.this.level= You already have an active project on this level. you.already.have.an.active.project.on.this.level= You already have an active project on this level.
partner.already.has.an.active.project.on.this.level= ${fullName} already has an active project on this level. partner.already.has.an.active.project.on.this.level= ${fullName} already has an active project on this level.
save= Submit idea
update= Update idea

@ -106,7 +106,25 @@ public class ProjectMyIdeasPanel extends Panel {
columns.add(new AbstractColumn<>(Model.of("Status"), "match.status") { columns.add(new AbstractColumn<>(Model.of("Status"), "match.status") {
@Override @Override
public void populateItem(Item<ICellPopulator<Idea>> item, String id, IModel<Idea> model) { public void populateItem(Item<ICellPopulator<Idea>> item, String id, IModel<Idea> model) {
item.add(new StudentIdeaStatusColumnPanel(id, model)); item.add(new Label(id, model.map(idea -> switch (idea.getMatchStatus()) {
case UNMATCHED -> "Submitted, waiting for matching by administrator";
case MATCHED -> {
if (applicationPeriodService.courseStartHasPassed(idea.getApplicationPeriod())) {
yield "Matched, project creation delayed. This is under investigation and handled manually. No action needed from you.";
} else {
yield "Matched, awaiting course start date";
}
}
case COMPLETED -> "Matched, project started";
case INACTIVE -> "Inactive";
})));
}
});
columns.add(new AbstractColumn<>(Model.of("Course start date")) {
@Override
public void populateItem(Item<ICellPopulator<Idea>> item, String id, IModel<Idea> iModel) {
item.add(new Label(id, iModel.map(Idea::getApplicationPeriod).map(ApplicationPeriod::getCourseStartDate)));
} }
}); });

@ -9,6 +9,7 @@ import se.su.dsv.scipro.activityplan.SupervisorActivityPlanPage;
import se.su.dsv.scipro.finalseminar.FinalSeminar; import se.su.dsv.scipro.finalseminar.FinalSeminar;
import se.su.dsv.scipro.finalseminar.ProjectFinalSeminarDetailsPage; import se.su.dsv.scipro.finalseminar.ProjectFinalSeminarDetailsPage;
import se.su.dsv.scipro.finalseminar.ProjectFinalSeminarPage; import se.su.dsv.scipro.finalseminar.ProjectFinalSeminarPage;
import se.su.dsv.scipro.finalseminar.ProjectOppositionPage;
import se.su.dsv.scipro.finalseminar.SupervisorFinalSeminarPage; import se.su.dsv.scipro.finalseminar.SupervisorFinalSeminarPage;
import se.su.dsv.scipro.forum.pages.ProjectForumBasePage; import se.su.dsv.scipro.forum.pages.ProjectForumBasePage;
import se.su.dsv.scipro.forum.pages.SupervisorForumBasePage; import se.su.dsv.scipro.forum.pages.SupervisorForumBasePage;
@ -170,6 +171,9 @@ public class NotificationLandingPage extends WebPage {
case FIRST_MEETING: case FIRST_MEETING:
defaultSplit.accept(ProjectFirstMeetingPage.class, SupervisorFirstMeetingPage.class); defaultSplit.accept(ProjectFirstMeetingPage.class, SupervisorFirstMeetingPage.class);
break; break;
case OPPOSITION_FAILED:
defaultSplit.accept(ProjectOppositionPage.class, SupervisorProjectDetailsPage.class);
break;
default: default:
// no specific redirect, will default to start page // no specific redirect, will default to start page
} }

@ -15,6 +15,9 @@
<p wicket:id="final_seminar_done_no_final_thesis_done_has_reflection"> <p wicket:id="final_seminar_done_no_final_thesis_done_has_reflection">
The final seminar has taken place, your final thesis has been rejected. You need to upload the new version of your final thesis to have your thesis assessed. The final seminar has taken place, your final thesis has been rejected. You need to upload the new version of your final thesis to have your thesis assessed.
</p> </p>
<p wicket:id="author_done_supervisor_not_done">
Final thesis and reflection uploaded.
</p>
<p wicket:id="all_done"> <p wicket:id="all_done">
Your thesis project is completed. Your thesis project is completed.
</p> </p>

@ -1,7 +1,5 @@
package se.su.dsv.scipro.project.panels; package se.su.dsv.scipro.project.panels;
import org.apache.wicket.Component;
import org.apache.wicket.behavior.Behavior;
import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.panel.GenericPanel; import org.apache.wicket.markup.html.panel.GenericPanel;
import org.apache.wicket.model.IModel; import org.apache.wicket.model.IModel;
@ -23,14 +21,18 @@ public class FinalThesisReflectionInstructionsPanel extends GenericPanel<Project
@Inject @Inject
private FinalSeminarService finalSeminarService; private FinalSeminarService finalSeminarService;
private final IModel<Boolean> hasSubmittedReflection;
private final IModel<Boolean> hasFinalThesis;
private final IModel<Boolean> hasHadFinalSeminar;
public FinalThesisReflectionInstructionsPanel(String id, IModel<Project> projectModel) { public FinalThesisReflectionInstructionsPanel(String id, IModel<Project> projectModel) {
super(id, projectModel); super(id, projectModel);
IModel<Boolean> hasSubmittedReflection = LoadableDetachableModel.of(() -> hasSubmittedReflection = LoadableDetachableModel.of(() ->
reflectionService.getSubmittedReflection(projectModel.getObject(), SciProSession.get().getUser()) != null); reflectionService.getSubmittedReflection(projectModel.getObject(), SciProSession.get().getUser()) != null);
IModel<Boolean> hasFinalThesis = LoadableDetachableModel.of(() -> hasFinalThesis = LoadableDetachableModel.of(() ->
!finalThesisService.isUploadAllowed(projectModel.getObject())); !finalThesisService.isUploadAllowed(projectModel.getObject()));
IModel<Boolean> hasHadFinalSeminar = LoadableDetachableModel.of(() -> hasHadFinalSeminar = LoadableDetachableModel.of(() ->
finalSeminarService.hasHadFinalSeminar(projectModel.getObject())); finalSeminarService.hasHadFinalSeminar(projectModel.getObject()));
add(new WebMarkupContainer("nothing_done") { add(new WebMarkupContainer("nothing_done") {
@Override @Override
@ -60,23 +62,29 @@ public class FinalThesisReflectionInstructionsPanel extends GenericPanel<Project
setVisible(hasHadFinalSeminar.getObject() && !hasFinalThesis.getObject() && hasSubmittedReflection.getObject()); setVisible(hasHadFinalSeminar.getObject() && !hasFinalThesis.getObject() && hasSubmittedReflection.getObject());
} }
}); });
add(new WebMarkupContainer("author_done_supervisor_not_done") {
@Override
protected void onConfigure() {
super.onConfigure();
boolean projectIsCompleted = projectModel.getObject().getProjectStatus() == ProjectStatus.COMPLETED;
setVisible(hasHadFinalSeminar.getObject() && hasFinalThesis.getObject() && hasSubmittedReflection.getObject() && !projectIsCompleted);
}
});
add(new WebMarkupContainer("all_done") { add(new WebMarkupContainer("all_done") {
@Override @Override
protected void onConfigure() { protected void onConfigure() {
super.onConfigure(); super.onConfigure();
setVisible(hasHadFinalSeminar.getObject() && hasFinalThesis.getObject() && hasSubmittedReflection.getObject()); boolean projectIsCompleted = projectModel.getObject().getProjectStatus() == ProjectStatus.COMPLETED;
} setVisible(hasHadFinalSeminar.getObject() && hasFinalThesis.getObject() && hasSubmittedReflection.getObject() && projectIsCompleted);
});
add(new Behavior() {
@Override
public void onConfigure(Component component) {
super.onConfigure(component);
component.setVisible(
!hasHadFinalSeminar.getObject()
|| !hasFinalThesis.getObject()
|| !hasSubmittedReflection.getObject()
|| projectModel.getObject().getProjectStatus() == ProjectStatus.COMPLETED);
} }
}); });
} }
@Override
protected void onDetach() {
hasFinalThesis.detach();
hasSubmittedReflection.detach();
hasHadFinalSeminar.detach();
super.onDetach();
}
} }

@ -228,8 +228,8 @@ public class RoughDraftApprovalDecisionPage extends ReviewerPage {
for (User author : authors.getObject()) { for (User author : authors.getObject()) {
try { try {
List<Examination> examinations = getPassFailExaminations(author); List<Examination> examinations = getPassFailExaminations(author);
if (examinations.isEmpty()) { if (examinations == null) {
// an empty list is returned if there's an error from the grading service // null is returned if there's an error from the grading service
return false; return false;
} }
} catch (RuntimeException e) { } catch (RuntimeException e) {

@ -1,6 +1,5 @@
package se.su.dsv.scipro.supervisor.pages; package se.su.dsv.scipro.supervisor.pages;
import com.google.common.base.Throwables;
import org.apache.wicket.Component; import org.apache.wicket.Component;
import org.apache.wicket.RestartResponseException; import org.apache.wicket.RestartResponseException;
import org.apache.wicket.markup.html.link.BookmarkablePageLink; import org.apache.wicket.markup.html.link.BookmarkablePageLink;
@ -9,8 +8,6 @@ import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.request.mapper.parameter.PageParameters;
import se.su.dsv.scipro.components.menuhighlighting.MenuHighlightSupervisorMyGroups; import se.su.dsv.scipro.components.menuhighlighting.MenuHighlightSupervisorMyGroups;
import se.su.dsv.scipro.file.FileService;
import se.su.dsv.scipro.forum.BasicForumService;
import se.su.dsv.scipro.forum.GroupForumService; import se.su.dsv.scipro.forum.GroupForumService;
import se.su.dsv.scipro.forum.dataobjects.GroupThread; import se.su.dsv.scipro.forum.dataobjects.GroupThread;
import se.su.dsv.scipro.forum.dataobjects.ForumThread; import se.su.dsv.scipro.forum.dataobjects.ForumThread;
@ -19,16 +16,11 @@ import se.su.dsv.scipro.group.GroupForumThread;
import se.su.dsv.scipro.util.PageParameterKeys; import se.su.dsv.scipro.util.PageParameterKeys;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import java.sql.SQLIntegrityConstraintViolationException;
public class SupervisorViewGroupThreadPage extends AbstractSupervisorGroupPage implements MenuHighlightSupervisorMyGroups { public class SupervisorViewGroupThreadPage extends AbstractSupervisorGroupPage implements MenuHighlightSupervisorMyGroups {
@Inject @Inject
private GroupForumService groupForumService; private GroupForumService groupForumService;
@Inject
private BasicForumService basicForumService;
@Inject
private FileService fileDescriptionService;
public SupervisorViewGroupThreadPage(final PageParameters parameters) { public SupervisorViewGroupThreadPage(final PageParameters parameters) {
super(parameters); super(parameters);
@ -48,21 +40,6 @@ public class SupervisorViewGroupThreadPage extends AbstractSupervisorGroupPage i
add(new FeedbackPanel("feedback")); add(new FeedbackPanel("feedback"));
try {
basicForumService.setThreadRead(loggedInUser(), groupThread.getForumThread(), true);
} catch (RuntimeException e) {
Throwable rootCause = Throwables.getRootCause(e);
if (rootCause instanceof SQLIntegrityConstraintViolationException) {
// One specific user keep getting weird constraint integrity violations.
// All attempts at replication have failed.
// To get rid of the error we catch it and ignore it. If we failed to
// mark a thread as read because it is already read, it does not matter.
}
else {
throw e;
}
}
add(new ViewForumThreadPanel<>("thread", groupThreadModel, new GroupForumThread(groupForumService)) { add(new ViewForumThreadPanel<>("thread", groupThreadModel, new GroupForumThread(groupForumService)) {
@Override @Override
protected Component newBackLink(final String id) { protected Component newBackLink(final String id) {

@ -3,7 +3,6 @@ package se.su.dsv.scipro.finalseminar;
import org.apache.wicket.Component; import org.apache.wicket.Component;
import org.apache.wicket.feedback.FencedFeedbackPanel; import org.apache.wicket.feedback.FencedFeedbackPanel;
import org.apache.wicket.model.Model; import org.apache.wicket.model.Model;
import org.jfree.data.time.Month;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -23,6 +22,7 @@ import se.su.dsv.scipro.test.ObjectMother;
import se.su.dsv.scipro.test.UserBuilder; import se.su.dsv.scipro.test.UserBuilder;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.Month;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Date; import java.util.Date;
@ -79,7 +79,7 @@ public class SeminarThesisPanelTest extends SciProTest {
@Test @Test
public void DeadlineInformationContainsDeadlineDate() { public void DeadlineInformationContainsDeadlineDate() {
// given // given
ZonedDateTime seminarDate = ZonedDateTime.of(2012, Month.APRIL, 21, 10, 10, 0, 0, ZoneId.systemDefault()); ZonedDateTime seminarDate = ZonedDateTime.of(2012, Month.APRIL.getValue(), 21, 10, 10, 0, 0, ZoneId.systemDefault());
int daysAhead = 10; int daysAhead = 10;
FinalSeminarSettings settings = new FinalSeminarSettings(); FinalSeminarSettings settings = new FinalSeminarSettings();

@ -1,5 +1,6 @@
package se.su.dsv.scipro.grading; package se.su.dsv.scipro.grading;
import org.apache.wicket.util.tester.FormTester;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import se.su.dsv.scipro.SciProTest; import se.su.dsv.scipro.SciProTest;
import se.su.dsv.scipro.file.FileDescription; import se.su.dsv.scipro.file.FileDescription;
@ -144,8 +145,9 @@ public class SendToExaminerTest extends SciProTest {
.thenReturn(Either.right(null)); .thenReturn(Either.right(null));
tester.startComponentInPage(new SendToExaminer("id", () -> project, () -> biden, () -> null)); tester.startComponentInPage(new SendToExaminer("id", () -> project, () -> biden, () -> null));
FormTester formTester = tester.newFormTester(path("id", "form"));
tester.clickLink(path("id", "send")); formTester.submit();
verify(gradingService).reportGrade(TOKEN, project.getIdentifier(), biden.getIdentifier(), gw.id(), grade.name(), finalThesis.getUploadDate()); verify(gradingService).reportGrade(TOKEN, project.getIdentifier(), biden.getIdentifier(), gw.id(), grade.name(), finalThesis.getUploadDate());
} }