Compare commits

..

13 Commits

Author SHA1 Message Date
819d5c7935 Attempt at deleting threads 2025-03-18 10:04:42 +01:00
6c91df6797 Only used by the view module 2025-03-18 10:04:42 +01:00
e4711971cc Deleting initial post removes entire thread 2025-03-18 10:04:42 +01:00
075c691ba3 Convert mocked tests to integration tests 2025-03-18 10:04:42 +01:00
6834ebaac1 Allow deletion of initial post 2025-03-18 10:04:42 +01:00
2ac30fa980 Add Checkstyle checking during Maven build ()
So far no rules are activated and it just puts the infrastructure in place.
Rules can be added in separately after discussion among the developers,
along with fixing any violations of the rules.

Co-authored-by: Nico Athanassiadis <nico@dsv.su.se>
Reviewed-on: 
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>
2025-03-18 09:12:41 +01:00
7504c267c5 Delete forum replies ()
Allows deleting (your own) forum replies.

Fixes 

## How to test

1. Log in as `eric@example.com` (supervisor) or as `sture@example.com` (author)
2. Open the forum in project "Putting the it in supervising"
3. Create a new thread
4. Post some replies as the different users
5. Delete the replies

Co-authored-by: Nico Athanassiadis <nico@dsv.su.se>
Reviewed-on: 
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>
2025-03-18 08:34:21 +01:00
6b77142a06 New Daisy API XSD ()
Allows a way to solve 

Reviewed-on: 
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>
2025-03-18 07:33:38 +01:00
59e3ec3fd9 Maintain project selection on validation failure during group creation ()
Fixes 

## 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: 
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>
2025-03-11 09:15:01 +01:00
9ede262e7b Fix crash due to JSON parsing on "Finishing up" tab ()
The seemingly unused library `jersey-hk2` that got removed in  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: 
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>
2025-03-11 08:49:16 +01:00
de18f200a7 Make exemptions for project type apply to partner as well ()
Read  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: 
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>
2025-03-10 07:17:28 +01:00
23e0a7f5ea Improvements to the Excel export of projects ()
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: 
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>
2025-03-05 14:07:47 +01:00
ed365bd7f5 Update library used to generate Excel files ()
Fixes 
Fixes 

## 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: 
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>
2025-03-05 13:01:20 +01:00
23 changed files with 283 additions and 35 deletions

@ -24,6 +24,7 @@ COPY test-data/src/ test-data/src/
RUN ./mvnw package \
--define skipTests \
--activate-profiles branch,DEV \
--define checkstyle.skip=true \
--define skip.npm \
--define skip.installnodenpm

7
checkstyle.xml Normal file

@ -0,0 +1,7 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
</module>

@ -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>

@ -32,5 +32,10 @@ public interface BasicForumService extends Serializable {
boolean canDelete(ForumPost forumPost);
void deletePost(ForumPost post);
Deletion deletePost(ForumPost post);
enum Deletion {
SINGLE_POST,
ENTIRE_THREAD,
}
}

@ -185,15 +185,17 @@ public class BasicForumServiceImpl implements BasicForumService {
@Override
@Transactional
public void deletePost(ForumPost post) {
public Deletion deletePost(ForumPost post) {
if (!canDelete(post)) {
throw new PostCantBeDeletedException();
}
if (isInitialPost(post)) {
threadRepository.delete(post.getForumThread());
return Deletion.ENTIRE_THREAD;
} else {
post.setDeleted(true);
postRepository.save(post);
return Deletion.SINGLE_POST;
}
}

@ -24,4 +24,8 @@ public interface ProjectForumService {
List<Pair<ProjectThread, ForumPost>> latestPost(Project a, int amount);
long getUnreadThreadsCount(Project project, User user);
boolean canDelete(ProjectThread projectThread, ForumPost forumPost);
void deletePost(ProjectThread projectThread, ForumPost forumPost);
}

@ -121,6 +121,20 @@ public class ProjectForumServiceImpl implements ProjectForumService {
return basicForumService.countUnreadThreads(list, user);
}
@Override
public boolean canDelete(ProjectThread projectThread, ForumPost forumPost) {
return basicForumService.canDelete(forumPost);
}
@Override
@Transactional
public void deletePost(ProjectThread projectThread, ForumPost forumPost) {
BasicForumService.Deletion deletion = basicForumService.deletePost(forumPost);
if (deletion == BasicForumService.Deletion.ENTIRE_THREAD) {
projectThreadRepository.delete(projectThread);
}
}
@Override
public ProjectThread findOne(long threadId) {
return projectThreadRepository.findOne(threadId);

@ -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()) {

@ -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>

@ -5,6 +5,7 @@ import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ -57,7 +58,10 @@ public class BasicForumServiceIntegrationTest extends IntegrationTest {
setLoggedInAs(commenter);
assertTrue(basicForumService.canDelete(reply));
assertDoesNotThrow(() -> basicForumService.deletePost(reply));
assertDoesNotThrow(() -> {
BasicForumService.Deletion deletion = basicForumService.deletePost(reply);
assertEquals(BasicForumService.Deletion.SINGLE_POST, deletion);
});
}
@Test
@ -204,7 +208,8 @@ public class BasicForumServiceIntegrationTest extends IntegrationTest {
setLoggedInAs(op);
basicForumService.deletePost(post);
BasicForumService.Deletion deletion = basicForumService.deletePost(post);
assertEquals(BasicForumService.Deletion.ENTIRE_THREAD, deletion);
ForumThread threadById = basicForumService.findThreadById(thread.getId());
assertNull(threadById, "Thread was not deleted");

@ -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();

@ -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);

24
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
@ -211,6 +211,11 @@
<artifactId>maven-war-plugin</artifactId>
<version>3.4.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.6.0</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
@ -312,6 +317,23 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<configuration>
<configLocation>checkstyle.xml</configLocation>
<failOnViolation>true</failOnViolation>
</configuration>
<executions>
<execution>
<id>validate</id>
<phase>process-sources</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

@ -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) {}
}

@ -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;

@ -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);
}
}

@ -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);
}
};
}

@ -10,4 +10,10 @@ public interface ForumThread<A> extends IClusterable {
ForumPost reply(A a, User poster, String content, Set<Attachment> attachments);
String getSubject(A a);
void setRead(User user, A a);
default boolean canDelete(A a, ForumPost forumPost) {
return false;
}
default void deletePost(A a, ForumPost forumPost) {}
}

@ -1,6 +1,5 @@
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;
@ -13,11 +12,9 @@ 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 {
@ -26,9 +23,6 @@ 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)));
@ -75,22 +69,23 @@ public class ForumPostPanel extends Panel {
@Override
public void onClick() {
ForumPost post = getModelObject();
basicForumService.deletePost(post);
onPostDeleted();
deletePost(post).run();
}
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(allowDeletion() && basicForumService.canDelete(getModelObject()));
setVisible(deletePost(getModelObject()) != null);
}
}
);
}
protected boolean allowDeletion() {
return false;
/**
* @param forumPost the post to delete
* @return a runnable that deletes the post, or {@code null} if it can not be deleted
*/
protected Runnable deletePost(ForumPost forumPost) {
return null;
}
protected void onPostDeleted() {}
}

@ -40,4 +40,14 @@ public class ProjectForumThread implements ForumThread<ProjectThread> {
public void setRead(User user, ProjectThread thread) {
projectForumService.markRead(user, thread);
}
@Override
public boolean canDelete(ProjectThread projectThread, ForumPost forumPost) {
return projectForumService.canDelete(projectThread, forumPost);
}
@Override
public void deletePost(ProjectThread projectThread, ForumPost forumPost) {
projectForumService.deletePost(projectThread, forumPost);
}
}

@ -62,14 +62,16 @@ public class ViewForumThreadPanel<A> extends GenericPanel<A> {
item.add(
new ForumPostPanel(POST, item.getModel()) {
@Override
protected boolean allowDeletion() {
return true;
}
@Override
protected void onPostDeleted() {
// Refresh the list of posts
listView.detach();
protected Runnable deletePost(ForumPost forumPost) {
if (forumThread.canDelete(ViewForumThreadPanel.this.getModelObject(), forumPost)) {
return () -> {
forumThread.deletePost(ViewForumThreadPanel.this.getModelObject(), forumPost);
// Refresh the list of posts
listView.detach();
};
} else {
return null;
}
}
}
);

@ -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()));

@ -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());
}
}
}
}