diff --git a/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarServiceImpl.java b/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarServiceImpl.java index 09deec221d..8f1a0a9a85 100755 --- a/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarServiceImpl.java +++ b/core/src/main/java/se/su/dsv/scipro/finalseminar/FinalSeminarServiceImpl.java @@ -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) { diff --git a/core/src/main/java/se/su/dsv/scipro/forum/BasicForumServiceImpl.java b/core/src/main/java/se/su/dsv/scipro/forum/BasicForumServiceImpl.java index 56dc06e357..05642970d2 100644 --- a/core/src/main/java/se/su/dsv/scipro/forum/BasicForumServiceImpl.java +++ b/core/src/main/java/se/su/dsv/scipro/forum/BasicForumServiceImpl.java @@ -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; } diff --git a/core/src/main/java/se/su/dsv/scipro/forum/ForumPostReadEvent.java b/core/src/main/java/se/su/dsv/scipro/forum/ForumPostReadEvent.java index 244f44737c..816c477c38 100644 --- a/core/src/main/java/se/su/dsv/scipro/forum/ForumPostReadEvent.java +++ b/core/src/main/java/se/su/dsv/scipro/forum/ForumPostReadEvent.java @@ -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) { } diff --git a/core/src/main/java/se/su/dsv/scipro/forum/ForumPostReadStateRepository.java b/core/src/main/java/se/su/dsv/scipro/forum/ForumPostReadStateRepository.java index 79f55f1c8d..d739c2bc0c 100644 --- a/core/src/main/java/se/su/dsv/scipro/forum/ForumPostReadStateRepository.java +++ b/core/src/main/java/se/su/dsv/scipro/forum/ForumPostReadStateRepository.java @@ -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); } diff --git a/core/src/main/java/se/su/dsv/scipro/forum/ForumPostReadStateRepositoryImpl.java b/core/src/main/java/se/su/dsv/scipro/forum/ForumPostReadStateRepositoryImpl.java index 9d324cc7ab..909c57693f 100644 --- a/core/src/main/java/se/su/dsv/scipro/forum/ForumPostReadStateRepositoryImpl.java +++ b/core/src/main/java/se/su/dsv/scipro/forum/ForumPostReadStateRepositoryImpl.java @@ -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); + } } diff --git a/core/src/main/java/se/su/dsv/scipro/forum/notifications/ForumNotifications.java b/core/src/main/java/se/su/dsv/scipro/forum/notifications/ForumNotifications.java index d0a9e19291..b294608351 100644 --- a/core/src/main/java/se/su/dsv/scipro/forum/notifications/ForumNotifications.java +++ b/core/src/main/java/se/su/dsv/scipro/forum/notifications/ForumNotifications.java @@ -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)); } } diff --git a/core/src/main/java/se/su/dsv/scipro/grading/GradingService.java b/core/src/main/java/se/su/dsv/scipro/grading/GradingService.java index b45da1614b..6172f3c280 100644 --- a/core/src/main/java/se/su/dsv/scipro/grading/GradingService.java +++ b/core/src/main/java/se/su/dsv/scipro/grading/GradingService.java @@ -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); diff --git a/core/src/main/java/se/su/dsv/scipro/grading/GradingServiceImpl.java b/core/src/main/java/se/su/dsv/scipro/grading/GradingServiceImpl.java index afe38b5de9..efd271ba90 100644 --- a/core/src/main/java/se/su/dsv/scipro/grading/GradingServiceImpl.java +++ b/core/src/main/java/se/su/dsv/scipro/grading/GradingServiceImpl.java @@ -46,7 +46,7 @@ public class GradingServiceImpl implements GradingService { return response.readEntity(EXAMINATION_LIST); } else { - return Collections.emptyList(); + return null; } } diff --git a/core/src/main/java/se/su/dsv/scipro/grading/PublicationMetadataService.java b/core/src/main/java/se/su/dsv/scipro/grading/PublicationMetadataService.java index 173f85b9cc..7e3118d786 100644 --- a/core/src/main/java/se/su/dsv/scipro/grading/PublicationMetadataService.java +++ b/core/src/main/java/se/su/dsv/scipro/grading/PublicationMetadataService.java @@ -6,4 +6,6 @@ public interface PublicationMetadataService { PublicationMetadata getByProject(Project project); void save(PublicationMetadata publicationMetadata); + + boolean hasSuppliedPublicationMetadata(Project project, boolean noNationalSubjectCategoriesAvailable); } diff --git a/core/src/main/java/se/su/dsv/scipro/grading/PublicationMetadataServiceImpl.java b/core/src/main/java/se/su/dsv/scipro/grading/PublicationMetadataServiceImpl.java index a610cc3731..26f0e1801b 100644 --- a/core/src/main/java/se/su/dsv/scipro/grading/PublicationMetadataServiceImpl.java +++ b/core/src/main/java/se/su/dsv/scipro/grading/PublicationMetadataServiceImpl.java @@ -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(); + } + } diff --git a/core/src/main/java/se/su/dsv/scipro/match/IdeaServiceImpl.java b/core/src/main/java/se/su/dsv/scipro/match/IdeaServiceImpl.java index 5ba42a3086..fdeae929fa 100755 --- a/core/src/main/java/se/su/dsv/scipro/match/IdeaServiceImpl.java +++ b/core/src/main/java/se/su/dsv/scipro/match/IdeaServiceImpl.java @@ -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()); } diff --git a/core/src/main/java/se/su/dsv/scipro/report/GradingReport.java b/core/src/main/java/se/su/dsv/scipro/report/GradingReport.java index 2765d30161..4c4b70397c 100644 --- a/core/src/main/java/se/su/dsv/scipro/report/GradingReport.java +++ b/core/src/main/java/se/su/dsv/scipro/report/GradingReport.java @@ -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; } diff --git a/core/src/main/java/se/su/dsv/scipro/report/GradingReportService.java b/core/src/main/java/se/su/dsv/scipro/report/GradingReportService.java index 507dc3025b..a2c1ac5c4e 100644 --- a/core/src/main/java/se/su/dsv/scipro/report/GradingReportService.java +++ b/core/src/main/java/se/su/dsv/scipro/report/GradingReportService.java @@ -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); } diff --git a/core/src/main/java/se/su/dsv/scipro/report/GradingReportServiceImpl.java b/core/src/main/java/se/su/dsv/scipro/report/GradingReportServiceImpl.java index 4ed4b2ab9c..b57d2bee0c 100644 --- a/core/src/main/java/se/su/dsv/scipro/report/GradingReportServiceImpl.java +++ b/core/src/main/java/se/su/dsv/scipro/report/GradingReportServiceImpl.java @@ -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) { diff --git a/core/src/main/resources/META-INF/persistence.xml b/core/src/main/resources/META-INF/persistence.xml index 6f68a20ae8..fc5f0a5977 100755 --- a/core/src/main/resources/META-INF/persistence.xml +++ b/core/src/main/resources/META-INF/persistence.xml @@ -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 --> diff --git a/core/src/main/resources/db/migration/V385__grading_report_date_submitted_to_examiner.sql b/core/src/main/resources/db/migration/V385__grading_report_date_submitted_to_examiner.sql new file mode 100644 index 0000000000..b188b7dbbc --- /dev/null +++ b/core/src/main/resources/db/migration/V385__grading_report_date_submitted_to_examiner.sql @@ -0,0 +1,2 @@ +alter table GradingReport + add date_submitted_to_examiner datetime null; diff --git a/core/src/test/java/se/su/dsv/scipro/forum/BasicForumServiceImplTest.java b/core/src/test/java/se/su/dsv/scipro/forum/BasicForumServiceImplTest.java index 21032ade4b..339b24080e 100644 --- a/core/src/test/java/se/su/dsv/scipro/forum/BasicForumServiceImplTest.java +++ b/core/src/test/java/se/su/dsv/scipro/forum/BasicForumServiceImplTest.java @@ -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 diff --git a/core/src/test/java/se/su/dsv/scipro/match/IdeaServiceImplTest.java b/core/src/test/java/se/su/dsv/scipro/match/IdeaServiceImplTest.java index 8672081365..c46e2bdc48 100755 --- a/core/src/test/java/se/su/dsv/scipro/match/IdeaServiceImplTest.java +++ b/core/src/test/java/se/su/dsv/scipro/match/IdeaServiceImplTest.java @@ -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 diff --git a/owasp.xml b/owasp.xml index bbf57a768c..d851f615c4 100644 --- a/owasp.xml +++ b/owasp.xml @@ -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> diff --git a/view/pom.xml b/view/pom.xml index 1321b7ce57..e3a1248be5 100644 --- a/view/pom.xml +++ b/view/pom.xml @@ -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> diff --git a/view/src/main/java/se/su/dsv/scipro/SciProApplication.java b/view/src/main/java/se/su/dsv/scipro/SciProApplication.java index 978a16437f..e5e2f46932 100755 --- a/view/src/main/java/se/su/dsv/scipro/SciProApplication.java +++ b/view/src/main/java/se/su/dsv/scipro/SciProApplication.java @@ -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() { diff --git a/view/src/main/java/se/su/dsv/scipro/admin/pages/AdminAssignReviewerPage.html b/view/src/main/java/se/su/dsv/scipro/admin/pages/AdminAssignReviewerPage.html index 361716a247..96e62706e2 100644 --- a/view/src/main/java/se/su/dsv/scipro/admin/pages/AdminAssignReviewerPage.html +++ b/view/src/main/java/se/su/dsv/scipro/admin/pages/AdminAssignReviewerPage.html @@ -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> diff --git a/view/src/main/java/se/su/dsv/scipro/admin/pages/AdminAssignReviewerPage.java b/view/src/main/java/se/su/dsv/scipro/admin/pages/AdminAssignReviewerPage.java index 3ff4d5de55..3d137d35b0 100644 --- a/view/src/main/java/se/su/dsv/scipro/admin/pages/AdminAssignReviewerPage.java +++ b/view/src/main/java/se/su/dsv/scipro/admin/pages/AdminAssignReviewerPage.java @@ -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 diff --git a/view/src/main/java/se/su/dsv/scipro/components/DisableSubmitButtonsOnSubmit.java b/view/src/main/java/se/su/dsv/scipro/components/DisableSubmitButtonsOnSubmit.java new file mode 100644 index 0000000000..3a98b9e500 --- /dev/null +++ b/view/src/main/java/se/su/dsv/scipro/components/DisableSubmitButtonsOnSubmit.java @@ -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)); + } +} \ No newline at end of file diff --git a/view/src/main/java/se/su/dsv/scipro/finalthesis/FinalThesisPanel$ApprovedPanel.html b/view/src/main/java/se/su/dsv/scipro/finalthesis/FinalThesisPanel$ApprovedPanel.html index c48ae4331c..d9558b986f 100644 --- a/view/src/main/java/se/su/dsv/scipro/finalthesis/FinalThesisPanel$ApprovedPanel.html +++ b/view/src/main/java/se/su/dsv/scipro/finalthesis/FinalThesisPanel$ApprovedPanel.html @@ -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> \ No newline at end of file diff --git a/view/src/main/java/se/su/dsv/scipro/finalthesis/FinalThesisPanel.java b/view/src/main/java/se/su/dsv/scipro/finalthesis/FinalThesisPanel.java index c05c498e6b..008bf88212 100644 --- a/view/src/main/java/se/su/dsv/scipro/finalthesis/FinalThesisPanel.java +++ b/view/src/main/java/se/su/dsv/scipro/finalthesis/FinalThesisPanel.java @@ -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 diff --git a/view/src/main/java/se/su/dsv/scipro/grading/CriteriaPanel.java b/view/src/main/java/se/su/dsv/scipro/grading/CriteriaPanel.java index b017bbc5c0..2dcfa3420a 100644 --- a/view/src/main/java/se/su/dsv/scipro/grading/CriteriaPanel.java +++ b/view/src/main/java/se/su/dsv/scipro/grading/CriteriaPanel.java @@ -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()); + } } } diff --git a/view/src/main/java/se/su/dsv/scipro/grading/GradingBasisPanel.utf8.properties b/view/src/main/java/se/su/dsv/scipro/grading/GradingBasisPanel.utf8.properties index 4067415139..16048d4403 100644 --- a/view/src/main/java/se/su/dsv/scipro/grading/GradingBasisPanel.utf8.properties +++ b/view/src/main/java/se/su/dsv/scipro/grading/GradingBasisPanel.utf8.properties @@ -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. diff --git a/view/src/main/java/se/su/dsv/scipro/grading/IndividualAuthorAssessment.html b/view/src/main/java/se/su/dsv/scipro/grading/IndividualAuthorAssessmentPanel.html similarity index 98% rename from view/src/main/java/se/su/dsv/scipro/grading/IndividualAuthorAssessment.html rename to view/src/main/java/se/su/dsv/scipro/grading/IndividualAuthorAssessmentPanel.html index ef28cf7891..1e31a1ac55 100644 --- a/view/src/main/java/se/su/dsv/scipro/grading/IndividualAuthorAssessment.html +++ b/view/src/main/java/se/su/dsv/scipro/grading/IndividualAuthorAssessmentPanel.html @@ -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"> diff --git a/view/src/main/java/se/su/dsv/scipro/grading/IndividualAuthorAssessment.java b/view/src/main/java/se/su/dsv/scipro/grading/IndividualAuthorAssessmentPanel.java similarity index 93% rename from view/src/main/java/se/su/dsv/scipro/grading/IndividualAuthorAssessment.java rename to view/src/main/java/se/su/dsv/scipro/grading/IndividualAuthorAssessmentPanel.java index c1bc061ead..d76881782b 100644 --- a/view/src/main/java/se/su/dsv/scipro/grading/IndividualAuthorAssessment.java +++ b/view/src/main/java/se/su/dsv/scipro/grading/IndividualAuthorAssessmentPanel.java @@ -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()); } diff --git a/view/src/main/java/se/su/dsv/scipro/grading/IndividualAuthorAssessment.utf8.properties b/view/src/main/java/se/su/dsv/scipro/grading/IndividualAuthorAssessmentPanel.utf8.properties similarity index 93% rename from view/src/main/java/se/su/dsv/scipro/grading/IndividualAuthorAssessment.utf8.properties rename to view/src/main/java/se/su/dsv/scipro/grading/IndividualAuthorAssessmentPanel.utf8.properties index d70f2b382d..29f7ff0ecc 100644 --- a/view/src/main/java/se/su/dsv/scipro/grading/IndividualAuthorAssessment.utf8.properties +++ b/view/src/main/java/se/su/dsv/scipro/grading/IndividualAuthorAssessmentPanel.utf8.properties @@ -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. diff --git a/view/src/main/java/se/su/dsv/scipro/grading/PublicationMetadataFormComponentPanel.html b/view/src/main/java/se/su/dsv/scipro/grading/PublicationMetadataFormComponentPanel.html index 23a1b6ca0b..416e983b21 100644 --- a/view/src/main/java/se/su/dsv/scipro/grading/PublicationMetadataFormComponentPanel.html +++ b/view/src/main/java/se/su/dsv/scipro/grading/PublicationMetadataFormComponentPanel.html @@ -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> \ No newline at end of file diff --git a/view/src/main/java/se/su/dsv/scipro/grading/PublicationMetadataFormComponentPanel.java b/view/src/main/java/se/su/dsv/scipro/grading/PublicationMetadataFormComponentPanel.java index 095a1b0dbd..aea827cb11 100644 --- a/view/src/main/java/se/su/dsv/scipro/grading/PublicationMetadataFormComponentPanel.java +++ b/view/src/main/java/se/su/dsv/scipro/grading/PublicationMetadataFormComponentPanel.java @@ -45,6 +45,7 @@ public class PublicationMetadataFormComponentPanel extends GenericPanel<Publicat .ifPresent(nationalSubjectCategoryChoice::setDefaultModelObject); } nationalSubjectCategoryChoice.setNullValid(true); + nationalSubjectCategoryChoice.setVisible(!availableCategories.getObject().isEmpty()); add(nationalSubjectCategoryChoice); } diff --git a/view/src/main/java/se/su/dsv/scipro/grading/SendToExaminer.html b/view/src/main/java/se/su/dsv/scipro/grading/SendToExaminer.html index a460fb8bf4..ed018da808 100644 --- a/view/src/main/java/se/su/dsv/scipro/grading/SendToExaminer.html +++ b/view/src/main/java/se/su/dsv/scipro/grading/SendToExaminer.html @@ -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> diff --git a/view/src/main/java/se/su/dsv/scipro/grading/SendToExaminer.java b/view/src/main/java/se/su/dsv/scipro/grading/SendToExaminer.java index ea08ce958d..3ccf7680a8 100644 --- a/view/src/main/java/se/su/dsv/scipro/grading/SendToExaminer.java +++ b/view/src/main/java/se/su/dsv/scipro/grading/SendToExaminer.java @@ -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, diff --git a/view/src/main/java/se/su/dsv/scipro/grading/SupervisorGradingPage.java b/view/src/main/java/se/su/dsv/scipro/grading/SupervisorGradingPage.java index 040e444e5a..695354d4c9 100644 --- a/view/src/main/java/se/su/dsv/scipro/grading/SupervisorGradingPage.java +++ b/view/src/main/java/se/su/dsv/scipro/grading/SupervisorGradingPage.java @@ -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( diff --git a/view/src/main/java/se/su/dsv/scipro/grading/SupervisorGradingReportPage.java b/view/src/main/java/se/su/dsv/scipro/grading/SupervisorGradingReportPage.java index 39bb707829..b4590290a6 100644 --- a/view/src/main/java/se/su/dsv/scipro/grading/SupervisorGradingReportPage.java +++ b/view/src/main/java/se/su/dsv/scipro/grading/SupervisorGradingReportPage.java @@ -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()); diff --git a/view/src/main/java/se/su/dsv/scipro/match/ProjectIdeaSubmissionPage.html b/view/src/main/java/se/su/dsv/scipro/match/ProjectIdeaSubmissionPage.html index 7a47688ae3..01e2105bc6 100755 --- a/view/src/main/java/se/su/dsv/scipro/match/ProjectIdeaSubmissionPage.html +++ b/view/src/main/java/se/su/dsv/scipro/match/ProjectIdeaSubmissionPage.html @@ -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> \ No newline at end of file diff --git a/view/src/main/java/se/su/dsv/scipro/match/ProjectIdeaSubmissionPanel.java b/view/src/main/java/se/su/dsv/scipro/match/ProjectIdeaSubmissionPanel.java index e94fd1df1b..ebc4a70d71 100755 --- a/view/src/main/java/se/su/dsv/scipro/match/ProjectIdeaSubmissionPanel.java +++ b/view/src/main/java/se/su/dsv/scipro/match/ProjectIdeaSubmissionPanel.java @@ -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); } diff --git a/view/src/main/java/se/su/dsv/scipro/match/ProjectIdeaSubmissionPanel.properties b/view/src/main/java/se/su/dsv/scipro/match/ProjectIdeaSubmissionPanel.properties index c11cc14769..f713e10b6b 100644 --- a/view/src/main/java/se/su/dsv/scipro/match/ProjectIdeaSubmissionPanel.properties +++ b/view/src/main/java/se/su/dsv/scipro/match/ProjectIdeaSubmissionPanel.properties @@ -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 diff --git a/view/src/main/java/se/su/dsv/scipro/match/ProjectMyIdeasPanel.java b/view/src/main/java/se/su/dsv/scipro/match/ProjectMyIdeasPanel.java index 8c42b37190..9841cbf78a 100755 --- a/view/src/main/java/se/su/dsv/scipro/match/ProjectMyIdeasPanel.java +++ b/view/src/main/java/se/su/dsv/scipro/match/ProjectMyIdeasPanel.java @@ -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))); } }); diff --git a/view/src/main/java/se/su/dsv/scipro/notifications/pages/NotificationLandingPage.java b/view/src/main/java/se/su/dsv/scipro/notifications/pages/NotificationLandingPage.java index e802947b73..b3b73a4bec 100644 --- a/view/src/main/java/se/su/dsv/scipro/notifications/pages/NotificationLandingPage.java +++ b/view/src/main/java/se/su/dsv/scipro/notifications/pages/NotificationLandingPage.java @@ -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 } diff --git a/view/src/main/java/se/su/dsv/scipro/project/panels/FinalThesisReflectionInstructionsPanel.html b/view/src/main/java/se/su/dsv/scipro/project/panels/FinalThesisReflectionInstructionsPanel.html index 42429c31a0..443adc9faf 100644 --- a/view/src/main/java/se/su/dsv/scipro/project/panels/FinalThesisReflectionInstructionsPanel.html +++ b/view/src/main/java/se/su/dsv/scipro/project/panels/FinalThesisReflectionInstructionsPanel.html @@ -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> diff --git a/view/src/main/java/se/su/dsv/scipro/project/panels/FinalThesisReflectionInstructionsPanel.java b/view/src/main/java/se/su/dsv/scipro/project/panels/FinalThesisReflectionInstructionsPanel.java index c390060c5f..c86cb81149 100644 --- a/view/src/main/java/se/su/dsv/scipro/project/panels/FinalThesisReflectionInstructionsPanel.java +++ b/view/src/main/java/se/su/dsv/scipro/project/panels/FinalThesisReflectionInstructionsPanel.java @@ -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(); + } } diff --git a/view/src/main/java/se/su/dsv/scipro/reviewer/RoughDraftApprovalDecisionPage.java b/view/src/main/java/se/su/dsv/scipro/reviewer/RoughDraftApprovalDecisionPage.java index 52a1455d59..b87df07354 100644 --- a/view/src/main/java/se/su/dsv/scipro/reviewer/RoughDraftApprovalDecisionPage.java +++ b/view/src/main/java/se/su/dsv/scipro/reviewer/RoughDraftApprovalDecisionPage.java @@ -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) { diff --git a/view/src/main/java/se/su/dsv/scipro/supervisor/pages/SupervisorViewGroupThreadPage.java b/view/src/main/java/se/su/dsv/scipro/supervisor/pages/SupervisorViewGroupThreadPage.java index 80bc74cf2a..840930e001 100644 --- a/view/src/main/java/se/su/dsv/scipro/supervisor/pages/SupervisorViewGroupThreadPage.java +++ b/view/src/main/java/se/su/dsv/scipro/supervisor/pages/SupervisorViewGroupThreadPage.java @@ -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) { diff --git a/view/src/test/java/se/su/dsv/scipro/finalseminar/SeminarThesisPanelTest.java b/view/src/test/java/se/su/dsv/scipro/finalseminar/SeminarThesisPanelTest.java index ba32119328..ac2a37fde4 100644 --- a/view/src/test/java/se/su/dsv/scipro/finalseminar/SeminarThesisPanelTest.java +++ b/view/src/test/java/se/su/dsv/scipro/finalseminar/SeminarThesisPanelTest.java @@ -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(); diff --git a/view/src/test/java/se/su/dsv/scipro/grading/SendToExaminerTest.java b/view/src/test/java/se/su/dsv/scipro/grading/SendToExaminerTest.java index 7976171fa6..a699f1e6ad 100644 --- a/view/src/test/java/se/su/dsv/scipro/grading/SendToExaminerTest.java +++ b/view/src/test/java/se/su/dsv/scipro/grading/SendToExaminerTest.java @@ -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()); }