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/main/java/se/su/dsv/scipro/CoreConfig.java b/core/src/main/java/se/su/dsv/scipro/CoreConfig.java index 352c43456a..bdcf367030 100644 --- a/core/src/main/java/se/su/dsv/scipro/CoreConfig.java +++ b/core/src/main/java/se/su/dsv/scipro/CoreConfig.java @@ -349,14 +349,16 @@ public class CoreConfig { ForumPostReadStateRepository readStateRepository, AbstractThreadRepository threadRepository, FileService fileService, - EventBus eventBus + EventBus eventBus, + CurrentUser currentUser ) { return new BasicForumServiceImpl( forumPostRepository, readStateRepository, threadRepository, fileService, - eventBus + eventBus, + currentUser ); } diff --git a/core/src/main/java/se/su/dsv/scipro/forum/BasicForumService.java b/core/src/main/java/se/su/dsv/scipro/forum/BasicForumService.java index 6a680ad26f..44ff9f407a 100644 --- a/core/src/main/java/se/su/dsv/scipro/forum/BasicForumService.java +++ b/core/src/main/java/se/su/dsv/scipro/forum/BasicForumService.java @@ -23,4 +23,12 @@ public interface BasicForumService extends Serializable { ForumThread createThread(String subject); long countUnreadThreads(List<ForumThread> forumThreadList, User user); + + ForumPost getLastPost(ForumThread forumThread); + + boolean hasAttachments(ForumThread forumThread); + + boolean canDelete(ForumPost forumPost); + + void deletePost(ForumPost post); } 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 c48e3cab91..c74a6fecab 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 @@ -10,6 +10,7 @@ import se.su.dsv.scipro.file.FileService; 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.system.CurrentUser; import se.su.dsv.scipro.system.User; public class BasicForumServiceImpl implements BasicForumService { @@ -19,6 +20,7 @@ public class BasicForumServiceImpl implements BasicForumService { private final ForumPostReadStateRepository readStateRepository; private final FileService fileService; private final EventBus eventBus; + private final CurrentUser currentUserProvider; @Inject public BasicForumServiceImpl( @@ -26,13 +28,15 @@ public class BasicForumServiceImpl implements BasicForumService { final ForumPostReadStateRepository readStateRepository, AbstractThreadRepository threadRepository, final FileService fileService, - final EventBus eventBus + final EventBus eventBus, + final CurrentUser currentUserProvider ) { this.postRepository = postRepository; this.readStateRepository = readStateRepository; this.threadRepository = threadRepository; this.fileService = fileService; this.eventBus = eventBus; + this.currentUserProvider = currentUserProvider; } @Override @@ -66,7 +70,7 @@ public class BasicForumServiceImpl implements BasicForumService { @Override public boolean isThreadRead(User user, ForumThread forumThread) { - for (ForumPost post : forumThread.getPosts()) { + for (ForumPost post : getPosts(forumThread)) { if (!getReadState(user, post).isRead()) { return false; } @@ -133,4 +137,56 @@ public class BasicForumServiceImpl implements BasicForumService { return post; } + + @Override + public ForumPost getLastPost(ForumThread forumThread) { + return Collections.max( + getPosts(forumThread), + Comparator.comparing(ForumPost::getDateCreated).thenComparing(ForumPost::getId) + ); + } + + @Override + public boolean hasAttachments(ForumThread forumThread) { + for (ForumPost post : getPosts(forumThread)) { + if (!post.getAttachments().isEmpty()) { + return true; + } + } + return false; + } + + @Override + public boolean canDelete(ForumPost forumPost) { + ForumPost initialPost = forumPost.getForumThread().getPosts().get(0); + if (forumPost.equals(initialPost)) { + // The initial post in a thread can never be deleted + return false; + } + + User user = currentUserProvider.get(); + // Current user can be null meaning the call came from the system + if (user == null) { + // Allow the system to delete any post + return true; + } + return Objects.equals(forumPost.getPostedBy(), user); + } + + @Override + @Transactional + public void deletePost(ForumPost post) { + if (!canDelete(post)) { + throw new PostCantBeDeletedException(); + } + post.setDeleted(true); + postRepository.save(post); + } + + private static final class PostCantBeDeletedException extends IllegalArgumentException { + + public PostCantBeDeletedException() { + super("User is not allowed to delete post"); + } + } } diff --git a/core/src/main/java/se/su/dsv/scipro/forum/dataobjects/ForumThread.java b/core/src/main/java/se/su/dsv/scipro/forum/dataobjects/ForumThread.java index 1c61c2c563..f1933e89c0 100644 --- a/core/src/main/java/se/su/dsv/scipro/forum/dataobjects/ForumThread.java +++ b/core/src/main/java/se/su/dsv/scipro/forum/dataobjects/ForumThread.java @@ -116,13 +116,4 @@ public class ForumThread extends LazyDeletableDomainObject { public User getCreatedBy() { return getPosts().get(0).getPostedBy(); } - - public boolean hasAttachments() { - for (ForumPost post : posts) { - if (!post.getAttachments().isEmpty()) { - return true; - } - } - return false; - } } 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/main/xsd/daisy_api.xsd b/core/src/main/xsd/daisy_api.xsd index fa68eef856..7ff97ec5a7 100755 --- a/core/src/main/xsd/daisy_api.xsd +++ b/core/src/main/xsd/daisy_api.xsd @@ -559,7 +559,7 @@ <xs:complexType name="course"> <xs:sequence> - <xs:element name="courseCode" type="xs:string" minOccurs="0"> + <xs:element name="courseCode" type="xs:string" minOccurs="1"> </xs:element> <xs:element name="credits" type="xs:float" minOccurs="1"> </xs:element> @@ -567,6 +567,8 @@ </xs:element> <xs:element name="level" type="educationalLevel" minOccurs="0"> </xs:element> + <xs:element name="degreeThesisCourse" type="xs:boolean" minOccurs="1"> + </xs:element> <xs:element name="eduInstDesignation" type="xs:string" minOccurs="1"> </xs:element> </xs:sequence> 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 de6288d6f4..6846f54411 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 @@ -121,6 +121,8 @@ public class BasicForumServiceImplTest { ForumThread forumThread = new ForumThread(); forumThread.addPost(post); + when(postRepository.findByThread(forumThread)).thenReturn(List.of(post)); + when(readStateRepository.find(eq(goodUser), isA(ForumPost.class))).thenReturn(readState); when(readStateRepository.find(eq(badUser), isA(ForumPost.class))).thenReturn(notReadState); diff --git a/core/src/test/java/se/su/dsv/scipro/forum/BasicForumServiceIntegrationTest.java b/core/src/test/java/se/su/dsv/scipro/forum/BasicForumServiceIntegrationTest.java new file mode 100644 index 0000000000..ec5d815af7 --- /dev/null +++ b/core/src/test/java/se/su/dsv/scipro/forum/BasicForumServiceIntegrationTest.java @@ -0,0 +1,83 @@ +package se.su.dsv.scipro.forum; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.inject.Inject; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import se.su.dsv.scipro.forum.dataobjects.ForumPost; +import se.su.dsv.scipro.forum.dataobjects.ForumThread; +import se.su.dsv.scipro.system.User; +import se.su.dsv.scipro.test.IntegrationTest; + +public class BasicForumServiceIntegrationTest extends IntegrationTest { + + @Inject + BasicForumService basicForumService; + + private User op; + private User commenter; + + @BeforeEach + public void setUp() { + User op = User.builder().firstName("Bill").lastName("Gates").emailAddress("bill@example.com").build(); + this.op = save(op); + + User commenter = User.builder().firstName("Steve").lastName("Jobs").emailAddress("steve@example.com").build(); + this.commenter = save(commenter); + } + + @Test + public void can_not_delete_original_post() { + ForumThread thread = basicForumService.createThread("Test thread"); + ForumPost originalPost = basicForumService.createReply(thread, op, "Test post", Set.of()); + + setLoggedInAs(op); + + assertFalse(basicForumService.canDelete(originalPost)); + assertThrows(IllegalArgumentException.class, () -> basicForumService.deletePost(originalPost)); + } + + @Test + public void can_delete_reply_to_original_post() { + ForumThread thread = basicForumService.createThread("Test thread"); + ForumPost originalPost = basicForumService.createReply(thread, op, "Test post", Set.of()); + ForumPost reply = basicForumService.createReply(thread, commenter, "Test reply", Set.of()); + + setLoggedInAs(commenter); + + assertTrue(basicForumService.canDelete(reply)); + assertDoesNotThrow(() -> basicForumService.deletePost(reply)); + } + + @Test + public void can_not_delete_someone_elses_reply() { + ForumThread thread = basicForumService.createThread("Test thread"); + ForumPost originalPost = basicForumService.createReply(thread, op, "Test post", Set.of()); + ForumPost reply = basicForumService.createReply(thread, commenter, "Test reply", Set.of()); + + setLoggedInAs(op); + + assertFalse(basicForumService.canDelete(reply)); + assertThrows(IllegalArgumentException.class, () -> basicForumService.deletePost(reply)); + } + + @Test + public void system_can_delete_all_replies() { + ForumThread thread = basicForumService.createThread("Test thread"); + ForumPost originalPost = basicForumService.createReply(thread, op, "Test post", Set.of()); + ForumPost reply = basicForumService.createReply(thread, commenter, "Test reply", Set.of()); + ForumPost secondReply = basicForumService.createReply(thread, op, "Test post", Set.of()); + + setLoggedInAs(null); + + assertTrue(basicForumService.canDelete(reply)); + assertDoesNotThrow(() -> basicForumService.deletePost(reply)); + assertTrue(basicForumService.canDelete(secondReply)); + assertDoesNotThrow(() -> basicForumService.deletePost(secondReply)); + } +} 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/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); diff --git a/core/src/test/java/se/su/dsv/scipro/test/SpringTest.java b/core/src/test/java/se/su/dsv/scipro/test/SpringTest.java index 2d52bd0d36..363fc8b80c 100644 --- a/core/src/test/java/se/su/dsv/scipro/test/SpringTest.java +++ b/core/src/test/java/se/su/dsv/scipro/test/SpringTest.java @@ -1,6 +1,7 @@ package se.su.dsv.scipro.test; import com.google.common.eventbus.EventBus; +import jakarta.inject.Inject; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.EntityTransaction; @@ -28,6 +29,7 @@ import se.su.dsv.scipro.RepositoryConfiguration; import se.su.dsv.scipro.profiles.CurrentProfile; import se.su.dsv.scipro.sukat.Sukat; import se.su.dsv.scipro.system.CurrentUser; +import se.su.dsv.scipro.system.User; @Testcontainers public abstract class SpringTest { @@ -38,6 +40,9 @@ public abstract class SpringTest { @Container static MariaDBContainer<?> mariaDBContainer = new MariaDBContainer<>("mariadb:10.11"); + @Inject + private TestUser testUser; + private CapturingEventBus capturingEventBus; @BeforeEach @@ -64,6 +69,8 @@ public abstract class SpringTest { annotationConfigApplicationContext.getBeanFactory().registerSingleton("entityManager", this.entityManager); annotationConfigApplicationContext.refresh(); annotationConfigApplicationContext.getAutowireCapableBeanFactory().autowireBean(this); + + testUser.setUser(null); // default to system } @AfterEach @@ -83,6 +90,10 @@ public abstract class SpringTest { } } + protected void setLoggedInAs(User user) { + this.testUser.setUser(user); + } + protected List<Object> getPublishedEvents() { return capturingEventBus.publishedEvents; } @@ -108,7 +119,7 @@ public abstract class SpringTest { @Bean public CurrentUser currentUser() { - return () -> null; + return new TestUser(); } @Bean @@ -129,4 +140,18 @@ public abstract class SpringTest { super.post(event); } } + + private static class TestUser implements CurrentUser { + + private User user; + + @Override + public User get() { + return user; + } + + private void setUser(User user) { + this.user = user; + } + } } 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); + } +} diff --git a/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ForumPostPanel.html b/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ForumPostPanel.html index eba931f634..b09f0405ca 100644 --- a/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ForumPostPanel.html +++ b/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ForumPostPanel.html @@ -7,9 +7,10 @@ <body> <wicket:panel> <div class="messageWrap"> - <div class="forumBlueBackground"> + <div class="forumBlueBackground d-flex justify-content-between"> <!-- DATE ROW--> - <wicket:container wicket:id="dateCreated"/> + <span wicket:id="dateCreated"></span> + <button wicket:id="delete" class="btn btn-sm btn-outline-danger ms-auto">Delete</button> </div> <div class="forumGrayBackground"> <div class="vertAlign"> diff --git a/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ForumPostPanel.java b/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ForumPostPanel.java index 004d99561a..225240d8e1 100644 --- a/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ForumPostPanel.java +++ b/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ForumPostPanel.java @@ -1,7 +1,9 @@ package se.su.dsv.scipro.forum.panels.threaded; +import jakarta.inject.Inject; import org.apache.wicket.Component; import org.apache.wicket.markup.html.WebMarkupContainer; +import org.apache.wicket.markup.html.link.Link; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.LambdaModel; @@ -11,9 +13,11 @@ import se.su.dsv.scipro.components.ListAdapterModel; import se.su.dsv.scipro.components.SmarterLinkMultiLineLabel; import se.su.dsv.scipro.data.enums.DateStyle; import se.su.dsv.scipro.file.FileReference; +import se.su.dsv.scipro.forum.BasicForumService; import se.su.dsv.scipro.forum.dataobjects.ForumPost; import se.su.dsv.scipro.profile.UserLinkPanel; import se.su.dsv.scipro.repository.panels.ViewAttachmentPanel; +import se.su.dsv.scipro.session.SciProSession; public class ForumPostPanel extends Panel { @@ -22,6 +26,9 @@ public class ForumPostPanel extends Panel { public static final String CONTENT = "content"; public static final String ATTACHMENT = "attachment"; + @Inject + private BasicForumService basicForumService; + public ForumPostPanel(String id, final IModel<ForumPost> model) { super(id); add(new UserLinkPanel(POSTED_BY, LambdaModel.of(model, ForumPost::getPostedBy, ForumPost::setPostedBy))); @@ -62,5 +69,28 @@ public class ForumPostPanel extends Panel { } } ); + + add( + new Link<>("delete", model) { + @Override + public void onClick() { + ForumPost post = getModelObject(); + basicForumService.deletePost(post); + onPostDeleted(); + } + + @Override + protected void onConfigure() { + super.onConfigure(); + setVisible(allowDeletion() && basicForumService.canDelete(getModelObject())); + } + } + ); } + + protected boolean allowDeletion() { + return false; + } + + protected void onPostDeleted() {} } diff --git a/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ThreadsOverviewPanel.java b/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ThreadsOverviewPanel.java index 02a9d00bd5..d0e7e1c55d 100644 --- a/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ThreadsOverviewPanel.java +++ b/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ThreadsOverviewPanel.java @@ -1,8 +1,7 @@ package se.su.dsv.scipro.forum.panels.threaded; +import jakarta.inject.Inject; import java.io.Serializable; -import java.util.Collections; -import java.util.Comparator; import java.util.List; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; @@ -15,6 +14,7 @@ import org.apache.wicket.model.LambdaModel; import org.apache.wicket.model.LoadableDetachableModel; import se.su.dsv.scipro.components.DateLabel; import se.su.dsv.scipro.data.enums.DateStyle; +import se.su.dsv.scipro.forum.BasicForumService; import se.su.dsv.scipro.forum.Discussable; import se.su.dsv.scipro.forum.dataobjects.ForumPost; import se.su.dsv.scipro.forum.dataobjects.ForumThread; @@ -23,6 +23,9 @@ import se.su.dsv.scipro.system.User; public class ThreadsOverviewPanel<A> extends Panel { + @Inject + private BasicForumService basicForumService; + public ThreadsOverviewPanel( final String id, final IModel<List<A>> model, @@ -41,7 +44,7 @@ public class ThreadsOverviewPanel<A> extends Panel { @Override protected void onConfigure() { super.onConfigure(); - setVisibilityAllowed(discussion.getObject().hasAttachments()); + setVisibilityAllowed(basicForumService.hasAttachments(discussion.getObject())); } } ); @@ -80,7 +83,7 @@ public class ThreadsOverviewPanel<A> extends Panel { BookmarkablePageLink<Void> newThreadLink(String id, IModel<A> thread); } - private static class LastPostColumn extends WebMarkupContainer { + private class LastPostColumn extends WebMarkupContainer { public LastPostColumn(String id, final IModel<ForumThread> model) { super(id); @@ -110,10 +113,7 @@ public class ThreadsOverviewPanel<A> extends Panel { return new LoadableDetachableModel<>() { @Override protected ForumPost load() { - return Collections.max( - model.getObject().getPosts(), - Comparator.comparing(ForumPost::getDateCreated).thenComparing(ForumPost::getId) - ); + return basicForumService.getLastPost(model.getObject()); } }; } diff --git a/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ViewForumThreadPanel.java b/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ViewForumThreadPanel.java index 9c62f77122..06b89d7e14 100644 --- a/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ViewForumThreadPanel.java +++ b/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ViewForumThreadPanel.java @@ -58,7 +58,21 @@ public class ViewForumThreadPanel<A> extends GenericPanel<A> { new ListView<>(POST_LIST, new PostProvider()) { @Override protected void populateItem(ListItem<ForumPost> item) { - item.add(new ForumPostPanel(POST, item.getModel())); + ListView<ForumPost> listView = this; + item.add( + new ForumPostPanel(POST, item.getModel()) { + @Override + protected boolean allowDeletion() { + return true; + } + + @Override + protected void onPostDeleted() { + // Refresh the list of posts + listView.detach(); + } + } + ); } } ); 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())); diff --git a/view/src/test/java/se/su/dsv/scipro/forum/panels/threaded/ThreadsOverviewPanelTest.java b/view/src/test/java/se/su/dsv/scipro/forum/panels/threaded/ThreadsOverviewPanelTest.java index 8c99db6c4f..614f92e7cd 100644 --- a/view/src/test/java/se/su/dsv/scipro/forum/panels/threaded/ThreadsOverviewPanelTest.java +++ b/view/src/test/java/se/su/dsv/scipro/forum/panels/threaded/ThreadsOverviewPanelTest.java @@ -9,6 +9,7 @@ import org.apache.wicket.model.Model; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import se.su.dsv.scipro.SciProTest; import se.su.dsv.scipro.forum.Discussable; @@ -20,11 +21,13 @@ import se.su.dsv.scipro.system.User; public class ThreadsOverviewPanelTest extends SciProTest { private List<ForumThread> threads; + private ForumPost post; @BeforeEach public void setUp() throws Exception { ForumThread forumThread = createThread(); threads = Arrays.asList(forumThread); + Mockito.when(basicForumService.getLastPost(forumThread)).thenReturn(post); } @Test @@ -54,7 +57,7 @@ public class ThreadsOverviewPanelTest extends SciProTest { private ForumThread createThread() { User bob = User.builder().firstName("Bob").lastName("the Builder").emailAddress("bob@building.com").build(); - ForumPost post = new ForumPost(); + post = new ForumPost(); post.setPostedBy(bob); ForumThread groupForumThread = new ForumThread(); groupForumThread.addPost(post);