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
@Transactional
public Either<SchedulingError, FinalSeminar> schedule(Project project, LocalDateTime when, FinalSeminarDetails details) {
if (project.isFinalSeminarRuleExempted()) {
return createSeminar(project, when, details);
@ -86,7 +87,14 @@ public class FinalSeminarServiceImpl extends AbstractServiceImpl<FinalSeminar, L
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) {

@ -51,8 +51,11 @@ public class BasicForumServiceImpl implements BasicForumService {
@Override
@Transactional
public boolean setThreadRead(User user, ForumThread forumThread, boolean read) {
for (ForumPost post : forumThread.getPosts()) {
setRead(user, post, read);
readStateRepository.setThreadRead(user, forumThread, read);
if (read) {
for (ForumPost post : forumThread.getPosts()) {
eventBus.post(new ForumPostReadEvent(post, user));
}
}
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.system.User;
public final class ForumPostReadEvent {
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;
}
public record ForumPostReadEvent(ForumPost post, User 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.ForumPostReadState;
import se.su.dsv.scipro.forum.dataobjects.ForumPostReadStateId;
import se.su.dsv.scipro.forum.dataobjects.ForumThread;
import se.su.dsv.scipro.system.User;
@Transactional
@ -13,4 +14,6 @@ public interface ForumPostReadStateRepository
extends JpaRepository<ForumPostReadState, ForumPostReadStateId>, QueryDslPredicateExecutor<ForumPostReadState> {
ForumPostReadState find(User user, ForumPost post);
void setThreadRead(User user, ForumThread forumThread, boolean read);
}

@ -1,8 +1,11 @@
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.ForumPostReadState;
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.system.GenericRepo;
import se.su.dsv.scipro.system.User;
@ -23,4 +26,21 @@ public class ForumPostReadStateRepositoryImpl extends GenericRepo<ForumPostReadS
public ForumPostReadState find(User user, ForumPost 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
@Transactional
public void forumPostRead(ForumPostReadEvent forumPostReadEvent) {
forumNotificationRepository.findByForumPost(forumPostReadEvent.getPost()).ifPresent(connection ->
notificationService.setRead(forumPostReadEvent.getUser(), connection.getNotificationEvent(), true));
forumNotificationRepository.findByForumPost(forumPostReadEvent.post()).ifPresent(connection ->
notificationService.setRead(forumPostReadEvent.user(), connection.getNotificationEvent(), true));
}
}

@ -7,6 +7,9 @@ import java.time.LocalDate;
import java.util.*;
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);
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);
}
else {
return Collections.emptyList();
return null;
}
}

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

@ -1,6 +1,7 @@
package se.su.dsv.scipro.grading;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.Language;
import jakarta.inject.Inject;
import java.util.Objects;
@ -29,4 +30,17 @@ class PublicationMetadataServiceImpl implements PublicationMetadataService {
public void save(PublicationMetadata 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 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 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 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";
@ -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.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 jakarta.persistence.*;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -29,6 +30,10 @@ public abstract class GradingReport extends Report {
@OneToMany(mappedBy = "gradingReport", cascade = {CascadeType.ALL})
private List<GradingCriterion> gradingCriteria = new ArrayList<>();
@Basic
@Column(name = "date_submitted_to_examiner")
private Instant dateSubmittedToExaminer;
protected GradingReport() {
// JPA
}
@ -37,6 +42,7 @@ public abstract class GradingReport extends Report {
public void submit() {
super.submit();
setState(State.FINALIZED);
setDateSubmittedToExaminer(Instant.now());
}
public Project getProject() {
@ -51,6 +57,14 @@ public abstract class GradingReport extends Report {
gradingCriteria.add(criterion);
}
public Instant getDateSubmittedToExaminer(){
return this.dateSubmittedToExaminer;
}
public void setDateSubmittedToExaminer(Instant dateSubmittedToExaminer) {
this.dateSubmittedToExaminer = dateSubmittedToExaminer;
}
public State getState() {
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.util.Either;
import java.time.Instant;
import java.util.List;
public interface GradingReportService extends GenericService<GradingReport, Long> {
@ -24,4 +25,6 @@ public interface GradingReportService extends GenericService<GradingReport, Long
GradingBasis getGradingBasis(Project project);
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.Provider;
import java.time.Clock;
import java.time.Instant;
import java.util.*;
@Named
@ -92,6 +93,16 @@ public class GradingReportServiceImpl extends AbstractServiceImpl<GradingReport,
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(
Language language,
GradingCriterion gc) {

@ -12,6 +12,9 @@
transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<non-jta-data-source>java:/comp/env/jdbc/sciproDS</non-jta-data-source>
<properties>
<property name="hibernate.show_sql" value="false"/>
</properties>
</persistence-unit>
<!-- 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.ForumPostReadState;
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.test.ForumBuilder;
import se.su.dsv.scipro.test.UserBuilder;
@ -35,10 +33,6 @@ public class BasicForumServiceImplTest {
@Mock
private ForumPostRepository postRepository;
@Mock
private MailEventService mailEventService;
@Mock
private ForumMailSettingsService mailSettingsService;
@Mock
private EventBus eventBus;
@InjectMocks
private BasicForumServiceImpl basicForumService;
@ -93,23 +87,20 @@ public class BasicForumServiceImplTest {
}
@Test
public void testMarkThreadRead() {
when(readStateRepository.find(any(User.class), any(ForumPost.class))).thenReturn(new ForumPostReadState());
when(readStateRepository.save(isA(ForumPostReadState.class))).thenAnswer(AdditionalAnswers.returnsFirstArg());
public void testMarkThreadReadPostsEvent() {
User user = new User();
ForumPost post = new ForumPost();
post.setContent("post 1");
ForumPost post2 = new ForumPost();
post2.setContent("post 2");
ForumThread forumThread = new ForumThread();
forumThread.addPost(post);
forumThread.addPost(post2);
basicForumService.setThreadRead(user, forumThread, true);
ArgumentCaptor<ForumPostReadState> captor = ArgumentCaptor.forClass(ForumPostReadState.class);
verify(readStateRepository, times(2)).save(captor.capture());
assertTrue(captor.getValue().isRead(), "Did not save correct read state");
verify(eventBus).post(new ForumPostReadEvent(post, user));
verify(eventBus).post(new ForumPostReadEvent(post2, user));
}
@Test
@ -181,8 +172,8 @@ public class BasicForumServiceImplTest {
verify(eventBus).post(captor.capture());
ForumPostReadEvent event = captor.getValue();
assertEquals(event.getPost(), post);
assertEquals(event.getUser(), user);
assertEquals(event.post(), post);
assertEquals(event.user(), user);
}
@Test

@ -204,7 +204,12 @@ public class IdeaServiceImplTest {
when(applicationPeriodService.getTypesForStudent(applicationPeriod, student))
.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

@ -51,4 +51,18 @@
</notes>
<cve>CVE-2023-35116</cve>
</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>

@ -64,6 +64,11 @@
<groupId>com.lowagie</groupId>
<artifactId>itext</artifactId>
</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>
</dependency>
<dependency>

@ -5,6 +5,7 @@ import org.apache.wicket.*;
import org.apache.wicket.authorization.strategies.CompoundAuthorizationStrategy;
import org.apache.wicket.csp.CSPDirective;
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.Response;
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.ProjectViewChecklistPage;
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.finalseminar.*;
import se.su.dsv.scipro.finalthesis.SupervisorFinalThesisListingPage;
@ -80,6 +82,7 @@ import se.su.dsv.scipro.util.AdditionalExceptionLogger;
import jakarta.inject.Inject;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
public class SciProApplication extends LifecycleManagedWebApplication {
@ -106,6 +109,12 @@ public class SciProApplication extends LifecycleManagedWebApplication {
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;
}
@ -150,6 +159,12 @@ public class SciProApplication extends LifecycleManagedWebApplication {
.add(CSPDirective.IMG_SRC, "data:");
WicketWebjars.install(this);
getComponentInstantiationListeners().add(component -> {
if (component instanceof Form) {
component.add(new DisableSubmitButtonsOnSubmit());
}
});
}
private void mountForumPage() {

@ -22,8 +22,10 @@
<dt>Research area</dt>
<dd wicket:id="research_area"></dd>
<dt>Language</dt>
<dd wicket:id="language"></dd>
<wicket:enclosure>
<dt>Language</dt>
<dd wicket:id="language"></dd>
</wicket:enclosure>
<wicket:enclosure>
<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("research_area", projectModel.map(Project::getResearchArea).map(ResearchArea::getTitle)));
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 DateLabel("requested_at", roughDraftApproval.map(RoughDraftApproval::getCurrentDecision).map(Decision::getRequested), DateStyle.DATETIME) {
@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>
<strong>Status:</strong> <span wicket:id="status"></span>
<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>
</body>
</html>

@ -3,6 +3,7 @@ package se.su.dsv.scipro.finalthesis;
import org.apache.wicket.feedback.FencedFeedbackPanel;
import org.apache.wicket.markup.html.WebMarkupContainer;
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.EnumChoiceRenderer;
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.project.Project;
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.session.SciProSession;
import se.su.dsv.scipro.system.ProjectModule;
@ -26,6 +28,8 @@ import se.su.dsv.scipro.util.PageParameterKeys;
import jakarta.inject.Inject;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;
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_DATE = "approvedDate";
public static final String NO_DECISION_PANEL = "noDecisionPanel";
public static final String SUBMITTED_TO_EXAMINER_TIMESTAMP = "submittedToExaminerTimestamp";
@Inject
private FinalThesisService finalThesisService;
@ -49,6 +54,8 @@ public class FinalThesisPanel extends GenericPanel<Project> {
private PublishingConsentService publishingConsentService;
@Inject
private ReflectionService reflectionService;
@Inject
private GradingReportService gradingReportService;
public FinalThesisPanel(String id, IModel<Project> project) {
super(id, project);
@ -96,9 +103,17 @@ public class FinalThesisPanel extends GenericPanel<Project> {
private class ApprovedPanel extends Panel {
public ApprovedPanel(String id) {
super(id);
add(new EnumLabel<>("status", getModel().map(Project::getProjectStatus)));
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

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

@ -1,4 +1,4 @@
save = Save
overall_motivation = Overall motivation
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>
<li wicket:id="status_final_thesis"></li>
<li wicket:id="status_plagiarism"></li>
<li wicket:id="status_publication_metadata"></li>
<li>
<div wicket:id="status_grading_basis">></div>
<ul wicket:id="grading_basis_missing">

@ -33,8 +33,10 @@ import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
public class IndividualAuthorAssessment extends GenericPanel<User> {
public class IndividualAuthorAssessmentPanel extends GenericPanel<User> {
@Inject
private NationalSubjectCategoryService nationalSubjectCategoryService;
@Inject
private GradingReportService gradingReportService;
@Inject
@ -43,10 +45,12 @@ public class IndividualAuthorAssessment extends GenericPanel<User> {
private FinalThesisService finalThesisService;
@Inject
private FinalSeminarService finalSeminarService;
@Inject
private PublicationMetadataService publicationMetadataService;
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);
this.projectModel = projectModel;
@ -73,6 +77,10 @@ public class IndividualAuthorAssessment extends GenericPanel<User> {
redGreen("status_plagiarism", hasSubmittedPlagiarismAnalysis,
"must_perform_plagiarism_check",
"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);
redGreen("status_grading_basis", hasFilledInGradingBasis,
"grading_basis_must_meet_minimum_requirements",
@ -169,6 +177,7 @@ public class IndividualAuthorAssessment extends GenericPanel<User> {
super.onConfigure();
setVisible(hasApprovedFinalThesis.getObject()
&& hasSubmittedPlagiarismAnalysis.getObject()
&& hasSuppliedPublicationMetadata.getObject()
&& hasFilledInGradingBasis.getObject()
&& hasFilledInIndividualAssessment.getObject());
}

@ -4,6 +4,8 @@ must_approve_final_thesis = You must approve the final thesis.
final_thesis_approved = Final thesis approved.
must_perform_plagiarism_check = You have to check the text matching report and perform a plagiarism analysis.
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_minimum_requirements_met = General criteria met.
individual_assessment_must_meet_minimum_requirements = Not all individual criteria are met.

@ -3,12 +3,12 @@
<body>
<wicket:panel>
<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>
</div>
<wicket:enclosure>
<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>
</div>
</wicket:enclosure>
@ -22,11 +22,13 @@
<input class="form-control" id="keywords_sv" wicket:id="keywords_sv">
</div>
</wicket:enclosure>
<div class="mb-3">
<label class="form-label" for="national_subject_category">National subject category</label>
<select class="form-select" id="national_subject_category" wicket:id="national_subject_category">
</select>
</div>
<wicket:enclosure>
<div class="mb-3">
<label class="form-label" for="national_subject_category">National subject category (required)</label>
<select class="form-select" id="national_subject_category" wicket:id="national_subject_category">
</select>
</div>
</wicket:enclosure>
</wicket:panel>
</body>
</html>

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

@ -2,9 +2,20 @@
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org" lang="en">
<body>
<wicket:panel>
<wicket:enclosure child="send">
<wicket:enclosure child="form">
<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>
<p class="card-text" wicket:id="already_sent">
<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.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.model.IModel;
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.file.FileDescription;
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.User;
import se.su.dsv.scipro.util.Either;
import se.su.dsv.scipro.util.JavascriptEventConfirmation;
import jakarta.inject.Inject;
import java.time.LocalDate;
@ -75,10 +79,16 @@ public class SendToExaminer extends GenericPanel<Project> {
super(id, projectModel);
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
public void onClick() {
sendToExaminer(getModelObject());
protected void onSubmit() {
super.onSubmit();
sendToExaminer(authorModel.getObject(), examinationDate.getObject());
}
@Override
@ -86,7 +96,20 @@ public class SendToExaminer extends GenericPanel<Project> {
super.onConfigure();
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") {
@Override
protected void onConfigure() {
@ -101,6 +124,10 @@ public class SendToExaminer extends GenericPanel<Project> {
String token = getSession().getMetaData(OAuth.TOKEN);
Project project = getModelObject();
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) {
if (examination.hasManyPassingGrades()) {
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;
}
private void sendToExaminer(User author) {
private void sendToExaminer(User author, LocalDate examinationDate) {
checkStepsMissing();
if (hasErrorMessage()) {
// some steps have not been completed
@ -125,6 +152,10 @@ public class SendToExaminer extends GenericPanel<Project> {
return;
}
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
.stream()
.filter(Examination::hasManyPassingGrades)
@ -138,7 +169,7 @@ public class SendToExaminer extends GenericPanel<Project> {
} else if (gradedExaminations.isEmpty()) {
getSession().info("Nothing to report on " + author.getFullName());
} else {
sendSuggestion(project, author, gradedExaminations.get(0));
sendSuggestion(project, author, gradedExaminations.get(0), examinationDate);
}
needsSending.detach();
}
@ -176,7 +207,7 @@ public class SendToExaminer extends GenericPanel<Project> {
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);
Either<GetGradeError, Optional<Result>> currentResult
@ -197,7 +228,6 @@ public class SendToExaminer extends GenericPanel<Project> {
GradeCalculator gradeCalculator = gradeCalculatorService.getSupervisorCalculator(project);
SupervisorGradingReport supervisorGradingReport = gradingReportService.getSupervisorGradingReport(getModelObject(), author);
GradingReport.Grade grade = gradeCalculator.getGrade(supervisorGradingReport);
LocalDate examinationDate = getExaminationDate(author, project, finalThesis);
Either<ReportGradeError, Void> reported =
gradingService.reportGrade(
token,

@ -56,7 +56,8 @@ public class SupervisorGradingPage extends AbstractSupervisorProjectDetailsPage
@Override
protected void populateItem(final ListItem<User> item) {
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);
item.add(new NonGradedPanel(

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

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

@ -4,6 +4,7 @@ import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior;
import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
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.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.link.ExternalLink;
@ -116,7 +117,7 @@ public class ProjectIdeaSubmissionPanel extends GenericPanel<Idea> {
addResearchAreaAndKeywordsSelection();
addContract();
addDeleteButton();
add(new Button("save"));
add(new Label("save", isNewIdea ? new ResourceModel("save") : new ResourceModel("update")));
}
@Override
@ -321,8 +322,13 @@ public class ProjectIdeaSubmissionPanel extends GenericPanel<Idea> {
@Override
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);
if (isNewIdea) {
getSession().success(getString("ideaSubmitted", Model.of(idea)));
} else {
getSession().success(getString("ideaUpdated", Model.of(idea)));
}
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.
keywordError= You need to select between 1 and 5 keywords.
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.
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 \
@ -35,3 +39,5 @@ programInfo= Select the program within the context of which you are doing this i
programDropDown.nullValid=Not within a program
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.
save= Submit idea
update= Update idea

@ -106,7 +106,25 @@ public class ProjectMyIdeasPanel extends Panel {
columns.add(new AbstractColumn<>(Model.of("Status"), "match.status") {
@Override
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.ProjectFinalSeminarDetailsPage;
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.forum.pages.ProjectForumBasePage;
import se.su.dsv.scipro.forum.pages.SupervisorForumBasePage;
@ -170,6 +171,9 @@ public class NotificationLandingPage extends WebPage {
case FIRST_MEETING:
defaultSplit.accept(ProjectFirstMeetingPage.class, SupervisorFirstMeetingPage.class);
break;
case OPPOSITION_FAILED:
defaultSplit.accept(ProjectOppositionPage.class, SupervisorProjectDetailsPage.class);
break;
default:
// no specific redirect, will default to start page
}

@ -15,6 +15,9 @@
<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.
</p>
<p wicket:id="author_done_supervisor_not_done">
Final thesis and reflection uploaded.
</p>
<p wicket:id="all_done">
Your thesis project is completed.
</p>

@ -1,7 +1,5 @@
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.panel.GenericPanel;
import org.apache.wicket.model.IModel;
@ -23,14 +21,18 @@ public class FinalThesisReflectionInstructionsPanel extends GenericPanel<Project
@Inject
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) {
super(id, projectModel);
IModel<Boolean> hasSubmittedReflection = LoadableDetachableModel.of(() ->
hasSubmittedReflection = LoadableDetachableModel.of(() ->
reflectionService.getSubmittedReflection(projectModel.getObject(), SciProSession.get().getUser()) != null);
IModel<Boolean> hasFinalThesis = LoadableDetachableModel.of(() ->
hasFinalThesis = LoadableDetachableModel.of(() ->
!finalThesisService.isUploadAllowed(projectModel.getObject()));
IModel<Boolean> hasHadFinalSeminar = LoadableDetachableModel.of(() ->
hasHadFinalSeminar = LoadableDetachableModel.of(() ->
finalSeminarService.hasHadFinalSeminar(projectModel.getObject()));
add(new WebMarkupContainer("nothing_done") {
@Override
@ -60,23 +62,29 @@ public class FinalThesisReflectionInstructionsPanel extends GenericPanel<Project
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") {
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(hasHadFinalSeminar.getObject() && hasFinalThesis.getObject() && hasSubmittedReflection.getObject());
}
});
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);
boolean projectIsCompleted = projectModel.getObject().getProjectStatus() == ProjectStatus.COMPLETED;
setVisible(hasHadFinalSeminar.getObject() && hasFinalThesis.getObject() && hasSubmittedReflection.getObject() && projectIsCompleted);
}
});
}
@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()) {
try {
List<Examination> examinations = getPassFailExaminations(author);
if (examinations.isEmpty()) {
// an empty list is returned if there's an error from the grading service
if (examinations == null) {
// null is returned if there's an error from the grading service
return false;
}
} catch (RuntimeException e) {

@ -1,6 +1,5 @@
package se.su.dsv.scipro.supervisor.pages;
import com.google.common.base.Throwables;
import org.apache.wicket.Component;
import org.apache.wicket.RestartResponseException;
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.request.mapper.parameter.PageParameters;
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.dataobjects.GroupThread;
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 jakarta.inject.Inject;
import java.sql.SQLIntegrityConstraintViolationException;
public class SupervisorViewGroupThreadPage extends AbstractSupervisorGroupPage implements MenuHighlightSupervisorMyGroups {
@Inject
private GroupForumService groupForumService;
@Inject
private BasicForumService basicForumService;
@Inject
private FileService fileDescriptionService;
public SupervisorViewGroupThreadPage(final PageParameters parameters) {
super(parameters);
@ -48,21 +40,6 @@ public class SupervisorViewGroupThreadPage extends AbstractSupervisorGroupPage i
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)) {
@Override
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.feedback.FencedFeedbackPanel;
import org.apache.wicket.model.Model;
import org.jfree.data.time.Month;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
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 java.time.LocalDate;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
@ -79,7 +79,7 @@ public class SeminarThesisPanelTest extends SciProTest {
@Test
public void DeadlineInformationContainsDeadlineDate() {
// 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;
FinalSeminarSettings settings = new FinalSeminarSettings();

@ -1,5 +1,6 @@
package se.su.dsv.scipro.grading;
import org.apache.wicket.util.tester.FormTester;
import org.junit.jupiter.api.Test;
import se.su.dsv.scipro.SciProTest;
import se.su.dsv.scipro.file.FileDescription;
@ -144,8 +145,9 @@ public class SendToExaminerTest extends SciProTest {
.thenReturn(Either.right(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());
}