From ed365bd7f56d67e2df1b9692296ef102f4fa5f20 Mon Sep 17 00:00:00 2001
From: Andreas Svanberg <andreass@dsv.su.se>
Date: Wed, 5 Mar 2025 13:01:20 +0100
Subject: [PATCH 1/5] Update library used to generate Excel files (#125)

Fixes #94
Fixes #93

## How to test
1. Log in as the default admin
2. Go to "Project management / Projects"
3. Click "Download as Excel" under the table
4. See that it's still a valid Excel-file

Reviewed-on: https://gitea.dsv.su.se/DMC/scipro/pulls/125
Reviewed-by: Nico Athanassiadis <nico@dsv.su.se>
Co-authored-by: Andreas Svanberg <andreass@dsv.su.se>
Co-committed-by: Andreas Svanberg <andreass@dsv.su.se>
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index f79b662c36..3473242939 100755
--- a/pom.xml
+++ b/pom.xml
@@ -26,7 +26,7 @@
         <wicket.version>10.4.0</wicket.version>
         <jakarta.persistence-api.version>3.1.0</jakarta.persistence-api.version>
         <querydsl.version>5.0.0</querydsl.version>
-        <poi.version>5.2.5</poi.version>
+        <poi.version>5.4.0</poi.version>
 
         <!--
             When updating spring-boot check if the transitive dependency on json-smart has been

From 23e0a7f5ead02dba3724ce7fdbdcb6f24d8b7d5b Mon Sep 17 00:00:00 2001
From: Andreas Svanberg <andreass@dsv.su.se>
Date: Wed, 5 Mar 2025 14:07:47 +0100
Subject: [PATCH 2/5] Improvements to the Excel export of projects (#126)

The research area column show the string "null" instead of being an empty cell for projects without a research area. This has been fixed everywhere and not just on the project export.

The reviewer column showed weird technical details (`User#toString()`) instead of the reviewers name.

## How to test
1. On `develop` branch
2. Log in as the default admin
3. Go to "Project management / Projects"
4. Click "Excel export" under the table
5. Open the file and see
6. Repeat 1-5 on this branch

Co-authored-by: Nico Athanassiadis <nico@dsv.su.se>
Reviewed-on: https://gitea.dsv.su.se/DMC/scipro/pulls/126
Reviewed-by: Nico Athanassiadis <nico@dsv.su.se>
Co-authored-by: Andreas Svanberg <andreass@dsv.su.se>
Co-committed-by: Andreas Svanberg <andreass@dsv.su.se>
---
 .../dsv/scipro/datatables/project/ProjectDataPanel.java   | 8 +++++---
 view/src/main/java/se/su/dsv/scipro/io/ExcelExporter.java | 4 +++-
 2 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/view/src/main/java/se/su/dsv/scipro/datatables/project/ProjectDataPanel.java b/view/src/main/java/se/su/dsv/scipro/datatables/project/ProjectDataPanel.java
index 04b5d7b3f8..e58b5cce94 100644
--- a/view/src/main/java/se/su/dsv/scipro/datatables/project/ProjectDataPanel.java
+++ b/view/src/main/java/se/su/dsv/scipro/datatables/project/ProjectDataPanel.java
@@ -1,6 +1,5 @@
 package se.su.dsv.scipro.datatables.project;
 
-import com.google.common.eventbus.EventBus;
 import jakarta.inject.Inject;
 import java.util.*;
 import org.apache.wicket.ajax.AjaxRequestTarget;
@@ -33,7 +32,6 @@ import se.su.dsv.scipro.components.datatables.MultipleUsersColumn;
 import se.su.dsv.scipro.components.datatables.UserColumn;
 import se.su.dsv.scipro.dataproviders.FilteredDataProvider;
 import se.su.dsv.scipro.datatables.AjaxCheckboxWrapper;
-import se.su.dsv.scipro.notifications.NotificationController;
 import se.su.dsv.scipro.profile.UserLinkPanel;
 import se.su.dsv.scipro.project.Project;
 import se.su.dsv.scipro.project.ProjectService;
@@ -45,7 +43,6 @@ import se.su.dsv.scipro.system.ProjectType;
 import se.su.dsv.scipro.system.ProjectTypeService;
 import se.su.dsv.scipro.system.ResearchArea;
 import se.su.dsv.scipro.system.User;
-import se.su.dsv.scipro.system.UserService;
 import se.su.dsv.scipro.util.PageParameterKeys;
 
 public class ProjectDataPanel extends Panel {
@@ -170,6 +167,11 @@ public class ProjectDataPanel extends Panel {
             ) {
                 cellItem.add(new ReviewerColumnCell(componentId, rowModel));
             }
+
+            @Override
+            public IModel<?> getDataModel(IModel<Project> rowModel) {
+                return rowModel.map(Project::getReviewer).map(User::getFullName);
+            }
         };
     }
 
diff --git a/view/src/main/java/se/su/dsv/scipro/io/ExcelExporter.java b/view/src/main/java/se/su/dsv/scipro/io/ExcelExporter.java
index 14946fc9a4..7e4edfd722 100644
--- a/view/src/main/java/se/su/dsv/scipro/io/ExcelExporter.java
+++ b/view/src/main/java/se/su/dsv/scipro/io/ExcelExporter.java
@@ -72,7 +72,9 @@ public class ExcelExporter extends AbstractDataExporter {
         for (int i = 0; i < columns.size(); i++) {
             Object cellValue = columns.get(i).getDataModel(data).getObject();
             Cell cell = row.createCell(i);
-            cell.setCellValue(String.valueOf(cellValue));
+            if (cellValue != null) {
+                cell.setCellValue(cellValue.toString());
+            }
         }
     }
 }

From de18f200a7b95ad9e1e7bc9026335f80bd2a4412 Mon Sep 17 00:00:00 2001
From: Andreas Svanberg <andreass@dsv.su.se>
Date: Mon, 10 Mar 2025 07:17:28 +0100
Subject: [PATCH 3/5] Make exemptions for project type apply to partner as well
 (#128)

Read #119 for context first

When authors select supervisor ideas during an open application period they are only allowed to select ideas corresponding to their degree type classification. This limitation can be lifted by giving the author an exemption for "Project type limitation" on the corresponding application period. However, this limitation is still enforced for any potential partner *even if* the have been given the same exemption. This change makes it so the exemption applies to any selected partner as well and not just the author selecting the supervisor idea.

## How to test
1. Log in as `oskar@example.com`
2. Go to "Ideas / My ideas" page
3. Click "Select from available ideas" on the application period "Supervisor ideas"
4. Open the one available idea
5. Try to select it with "Johan Student" as a partner
6. Log in as admin
7. Go to "Match / Application periods"
8. Click "Edit exemptions" on the "Supervisor ideas" period
9. Give "Johan Student" an exemption to "Project type limitation"
10. Repeat steps 1-5

Reviewed-on: https://gitea.dsv.su.se/DMC/scipro/pulls/128
Reviewed-by: Nico Athanassiadis <nico@dsv.su.se>
Co-authored-by: Andreas Svanberg <andreass@dsv.su.se>
Co-committed-by: Andreas Svanberg <andreass@dsv.su.se>
---
 .../su/dsv/scipro/match/IdeaServiceImpl.java  |  6 +-
 .../dsv/scipro/match/IdeaServiceImplTest.java | 34 ++++++++++
 .../se/su/dsv/scipro/testdata/BaseData.java   | 10 +++
 .../dsv/scipro/testdata/DataInitializer.java  |  5 ++
 .../populators/PartnerTypeExemption.java      | 68 +++++++++++++++++++
 5 files changed, 119 insertions(+), 4 deletions(-)
 create mode 100644 test-data/src/main/java/se/su/dsv/scipro/testdata/populators/PartnerTypeExemption.java

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 08845a8a3f..2e97a2b07a 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
@@ -235,10 +235,8 @@ public class IdeaServiceImpl extends AbstractServiceImpl<Idea, Long> implements
             if (authorParticipatingOnActiveIdea(coAuthor, ap)) {
                 return new Pair<>(Boolean.FALSE, PARTNER_ALREADY_PARTICIPATING_ERROR);
             }
-            if (
-                coAuthor.getDegreeType() != ProjectType.UNKNOWN &&
-                coAuthor.getDegreeType() != idea.getProjectType().getDegreeType()
-            ) {
+            List<ProjectType> typesForCoAuthor = applicationPeriodService.getTypesForStudent(ap, coAuthor);
+            if (!typesForCoAuthor.contains(idea.getProjectType())) {
                 return new Pair<>(Boolean.FALSE, WRONG_LEVEL_FOR_YOUR_PARTNER);
             }
             if (!projectService.getActiveProjectsByUserAndProjectType(coAuthor, idea.getProjectType()).isEmpty()) {
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 6365ba546a..871ea15351 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
@@ -241,6 +241,7 @@ public class IdeaServiceImplTest {
         when(generalSystemSettingsService.getGeneralSystemSettingsInstance()).thenReturn(new GeneralSystemSettings());
         Idea idea = createBachelorIdea(Idea.Status.UNMATCHED);
         when(applicationPeriodService.getTypesForStudent(applicationPeriod, student)).thenReturn(List.of(bachelor));
+        when(applicationPeriodService.getTypesForStudent(applicationPeriod, coAuthor)).thenReturn(List.of(bachelor));
 
         Pair<Boolean, String> acceptance = ideaService.validateStudentAcceptance(
             idea,
@@ -401,6 +402,39 @@ public class IdeaServiceImplTest {
         assertEquals(expected, ideaService.countAuthorsByApplicationPeriod(applicationPeriod, params));
     }
 
+    @Test
+    public void wrong_type_for_author() {
+        when(applicationPeriodService.getTypesForStudent(applicationPeriod, student)).thenReturn(List.of(master));
+        when(applicationPeriodService.getTypesForStudent(applicationPeriod, coAuthor)).thenReturn(List.of(bachelor));
+
+        assertPair(
+            false,
+            "The idea is the wrong level for you, please pick another one.",
+            ideaService.validateStudentAcceptance(
+                createBachelorIdea(Idea.Status.UNMATCHED),
+                student,
+                coAuthor,
+                applicationPeriod
+            )
+        );
+    }
+
+    @Test
+    public void wrong_type_for_partner() {
+        when(applicationPeriodService.getTypesForStudent(applicationPeriod, coAuthor)).thenReturn(List.of(master));
+
+        assertPair(
+            false,
+            "The idea is the wrong level for your partner, please pick another one.",
+            ideaService.validateStudentAcceptance(
+                createBachelorIdea(Idea.Status.UNMATCHED),
+                student,
+                coAuthor,
+                applicationPeriod
+            )
+        );
+    }
+
     private Idea mockInactiveIdea() {
         Idea idea = new Idea();
         Match match = new Match();
diff --git a/test-data/src/main/java/se/su/dsv/scipro/testdata/BaseData.java b/test-data/src/main/java/se/su/dsv/scipro/testdata/BaseData.java
index 1dd6b18132..56bb1313a4 100644
--- a/test-data/src/main/java/se/su/dsv/scipro/testdata/BaseData.java
+++ b/test-data/src/main/java/se/su/dsv/scipro/testdata/BaseData.java
@@ -1,6 +1,9 @@
 package se.su.dsv.scipro.testdata;
 
+import java.util.List;
+import se.su.dsv.scipro.match.Keyword;
 import se.su.dsv.scipro.system.ProjectType;
+import se.su.dsv.scipro.system.ResearchArea;
 
 /// All the base test data that can be re-used in different test cases.
 ///
@@ -16,4 +19,11 @@ public interface BaseData {
     ProjectType bachelor();
     ProjectType magister();
     ProjectType master();
+
+    /**
+     * @return generic research area with some keywords attached to it
+     */
+    ResearchAreaAndKeywords researchArea();
+
+    record ResearchAreaAndKeywords(ResearchArea researchArea, List<Keyword> keywords) {}
 }
diff --git a/test-data/src/main/java/se/su/dsv/scipro/testdata/DataInitializer.java b/test-data/src/main/java/se/su/dsv/scipro/testdata/DataInitializer.java
index cf01ab2315..c12cb602b2 100644
--- a/test-data/src/main/java/se/su/dsv/scipro/testdata/DataInitializer.java
+++ b/test-data/src/main/java/se/su/dsv/scipro/testdata/DataInitializer.java
@@ -2193,6 +2193,11 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
         return createEmployee(firstName);
     }
 
+    @Override
+    public ResearchAreaAndKeywords researchArea() {
+        return new ResearchAreaAndKeywords(researchArea1, List.of(keyword1, keyword2));
+    }
+
     private static final class SimpleTextFile implements FileUpload {
 
         private final User uploader;
diff --git a/test-data/src/main/java/se/su/dsv/scipro/testdata/populators/PartnerTypeExemption.java b/test-data/src/main/java/se/su/dsv/scipro/testdata/populators/PartnerTypeExemption.java
new file mode 100644
index 0000000000..c490d0b069
--- /dev/null
+++ b/test-data/src/main/java/se/su/dsv/scipro/testdata/populators/PartnerTypeExemption.java
@@ -0,0 +1,68 @@
+package se.su.dsv.scipro.testdata.populators;
+
+import jakarta.inject.Inject;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.Set;
+import org.springframework.stereotype.Service;
+import se.su.dsv.scipro.match.ApplicationPeriod;
+import se.su.dsv.scipro.match.ApplicationPeriodService;
+import se.su.dsv.scipro.match.Idea;
+import se.su.dsv.scipro.match.IdeaService;
+import se.su.dsv.scipro.match.Target;
+import se.su.dsv.scipro.match.TargetService;
+import se.su.dsv.scipro.system.User;
+import se.su.dsv.scipro.testdata.BaseData;
+import se.su.dsv.scipro.testdata.Factory;
+import se.su.dsv.scipro.testdata.TestDataPopulator;
+
+@Service
+public class PartnerTypeExemption implements TestDataPopulator {
+
+    private final ApplicationPeriodService applicationPeriodService;
+    private final IdeaService ideaService;
+    private final TargetService targetService;
+
+    @Inject
+    public PartnerTypeExemption(
+        ApplicationPeriodService applicationPeriodService,
+        IdeaService ideaService,
+        TargetService targetService
+    ) {
+        this.applicationPeriodService = applicationPeriodService;
+        this.ideaService = ideaService;
+        this.targetService = targetService;
+    }
+
+    @Override
+    public void populate(BaseData baseData, Factory factory) {
+        factory.createAuthor("Oskar");
+
+        User johan = factory.createAuthor("Johan");
+        johan.setDegreeType(baseData.master().getDegreeType());
+
+        User supervisor = factory.createSupervisor("Elsa");
+
+        ApplicationPeriod applicationPeriod = new ApplicationPeriod("Supervisor ideas");
+        applicationPeriod.setStartDate(LocalDate.now());
+        applicationPeriod.setEndDate(LocalDate.now().plusDays(14));
+        applicationPeriod.setCourseStartDateTime(LocalDateTime.now().plusDays(15));
+        applicationPeriod.setProjectTypes(Set.of(baseData.bachelor()));
+        applicationPeriodService.save(applicationPeriod);
+
+        Target target = targetService.findOne(applicationPeriod, supervisor, baseData.bachelor());
+        target.setTarget(10);
+        targetService.save(target);
+
+        Idea idea = new Idea();
+        idea.setPublished(true);
+        idea.setTitle("The next gen AI 2.0 turbo edition");
+        idea.setPrerequisites("Hacker experience");
+        idea.setDescription("Better than all the rest");
+        idea.setProjectType(baseData.bachelor());
+        idea.setApplicationPeriod(applicationPeriod);
+        idea.setResearchArea(baseData.researchArea().researchArea());
+
+        ideaService.saveSupervisorIdea(idea, supervisor, baseData.researchArea().keywords(), true);
+    }
+}

From 9ede262e7bd7767933e0ce4a349bb73436a8786b Mon Sep 17 00:00:00 2001
From: Andreas Svanberg <andreass@dsv.su.se>
Date: Tue, 11 Mar 2025 08:49:16 +0100
Subject: [PATCH 4/5] Fix crash due to JSON parsing on "Finishing up" tab
 (#132)

The seemingly unused library `jersey-hk2` that got removed in #118 is used, if present, internally by the Jersey client to find and register Jackson modules (such as those that provide `java.time` support).

## How to test
1. Turn on the `DAISY-INTEGRATION` Maven profile (alongside `DEV`)
2. Configure some projects and their authors to have a Daisy connection
3. Log in as the supervisor
4. Go to the "Finishing up" tab in the project
5. See that it works compared to `develop` branch

Reviewed-on: https://gitea.dsv.su.se/DMC/scipro/pulls/132
Reviewed-by: Nico Athanassiadis <nico@dsv.su.se>
Co-authored-by: Andreas Svanberg <andreass@dsv.su.se>
Co-committed-by: Andreas Svanberg <andreass@dsv.su.se>
---
 core/pom.xml                                  |  4 ++
 ...adingReportServiceImplIntegrationTest.java | 45 +++++++++++++++++++
 2 files changed, 49 insertions(+)

diff --git a/core/pom.xml b/core/pom.xml
index d2a9506ee7..cda6ec6cdd 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -32,6 +32,10 @@
             <groupId>org.glassfish.jersey.media</groupId>
             <artifactId>jersey-media-json-jackson</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.inject</groupId>
+            <artifactId>jersey-hk2</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-context</artifactId>
diff --git a/core/src/test/java/se/su/dsv/scipro/report/GradingReportServiceImplIntegrationTest.java b/core/src/test/java/se/su/dsv/scipro/report/GradingReportServiceImplIntegrationTest.java
index 597396bbea..63808671e3 100644
--- a/core/src/test/java/se/su/dsv/scipro/report/GradingReportServiceImplIntegrationTest.java
+++ b/core/src/test/java/se/su/dsv/scipro/report/GradingReportServiceImplIntegrationTest.java
@@ -6,7 +6,11 @@ import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import com.google.common.eventbus.EventBus;
+import com.sun.net.httpserver.HttpServer;
 import jakarta.inject.Inject;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
 import java.time.LocalDate;
 import java.time.Month;
 import java.util.*;
@@ -15,6 +19,9 @@ import org.junit.jupiter.api.Test;
 import se.su.dsv.scipro.finalseminar.FinalSeminar;
 import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition;
 import se.su.dsv.scipro.finalseminar.OppositionApprovedEvent;
+import se.su.dsv.scipro.grading.GetGradeError;
+import se.su.dsv.scipro.grading.GradingServiceImpl;
+import se.su.dsv.scipro.grading.Result;
 import se.su.dsv.scipro.project.Project;
 import se.su.dsv.scipro.security.auth.roles.Roles;
 import se.su.dsv.scipro.system.DegreeType;
@@ -149,6 +156,44 @@ public class GradingReportServiceImplIntegrationTest extends IntegrationTest {
         assertNull(oppositionCriterion.getFeedback());
     }
 
+    @Test
+    public void test_json_deserialization() throws IOException {
+        String json =
+            """
+            {
+                "grade": "A",
+                "reported": "2021-01-01"
+            }
+            """;
+        HttpServer httpServer = startHttpServerWithJsonResponse(json);
+
+        int port = httpServer.getAddress().getPort();
+
+        GradingServiceImpl gradingService = new GradingServiceImpl("http://localhost:" + port);
+        Either<GetGradeError, Optional<Result>> result = gradingService.getResult("token", 1, 2, 3);
+
+        Optional<Result> right = result.right();
+        assertTrue(right.isPresent());
+        assertEquals(LocalDate.of(2021, 1, 1), right.get().reported());
+
+        httpServer.stop(0);
+    }
+
+    private static HttpServer startHttpServerWithJsonResponse(String json) throws IOException {
+        HttpServer httpServer = HttpServer.create();
+        httpServer.createContext("/", exchange -> {
+            try (exchange) {
+                byte[] response = json.getBytes(StandardCharsets.UTF_8);
+                exchange.getResponseHeaders().add("Content-Type", "application/json");
+                exchange.sendResponseHeaders(200, response.length);
+                exchange.getResponseBody().write(response);
+            }
+        });
+        httpServer.bind(new InetSocketAddress("localhost", 0), 0);
+        httpServer.start();
+        return httpServer;
+    }
+
     private void addOppositionCriterion() {
         gradingReportTemplate = createOppositionCriteria(gradingReportTemplate, 2);
         gradingReport = createGradingReport(project, student);

From 59e3ec3fd940bf257546d67301233979c27de239 Mon Sep 17 00:00:00 2001
From: Andreas Svanberg <andreass@dsv.su.se>
Date: Tue, 11 Mar 2025 09:15:01 +0100
Subject: [PATCH 5/5] Maintain project selection on validation failure during
 group creation (#133)

Fixes #129

## How to test
1. Log in as `evan@example.com`
2. Go to "My groups"
3. Click "Create new group"
4. Select some projects but do *not* fill in the "Title"
5. Click save
6. Error message should be presented
7. Project selection should be maintained

Co-authored-by: Nico Athanassiadis <nico@dsv.su.se>
Reviewed-on: https://gitea.dsv.su.se/DMC/scipro/pulls/133
Reviewed-by: Nico Athanassiadis <nico@dsv.su.se>
Co-authored-by: Andreas Svanberg <andreass@dsv.su.se>
Co-committed-by: Andreas Svanberg <andreass@dsv.su.se>
---
 .../src/main/java/se/su/dsv/scipro/group/EditGroupPanel.java | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/view/src/main/java/se/su/dsv/scipro/group/EditGroupPanel.java b/view/src/main/java/se/su/dsv/scipro/group/EditGroupPanel.java
index 831986ffad..05b8396ffa 100644
--- a/view/src/main/java/se/su/dsv/scipro/group/EditGroupPanel.java
+++ b/view/src/main/java/se/su/dsv/scipro/group/EditGroupPanel.java
@@ -59,6 +59,11 @@ public class EditGroupPanel extends Panel {
             });
             add(
                 new ListView<>("available_projects", availableProjects) {
+                    {
+                        // must re-use list items to maintain form component (checkboxes) state
+                        setReuseItems(true);
+                    }
+
                     @Override
                     protected void populateItem(ListItem<Project> item) {
                         CheckBox checkbox = new CheckBox("selected", new SelectProjectModel(model, item.getModel()));