From 27204cba5e9088e33cf0ff5b9baaaf478f6c384a Mon Sep 17 00:00:00 2001
From: Andreas Svanberg <andreass@dsv.su.se>
Date: Tue, 18 Mar 2025 15:58:27 +0100
Subject: [PATCH] Start of the big epic refactor

The end purpose is to enable removal of entire threads. Previous attempts have failed, all because of something JPA/Hibernate related such as not-null constraint, transient/detached entities, or something else.

 The approach is to get rid of @Entitys throughout the system and use data transfer objects (DTOs) and to make the thread more of a central concept rather than an empty container with an "initial" post. This should allow a more dedicated "delete thread" method that can take care of all the issues instead of "is this the last post then do extra steps" which is causing the problems.

 Changing to DTOs will also align better with the technical future that the current developers want and have decided on "Never return @Entity classes from services" (https://gitea.dsv.su.se/DMC/scipro/wiki/Technical-future).
---
 .../dsv/scipro/forum/GroupForumService.java   |  2 +
 .../scipro/forum/GroupForumServiceImpl.java   |  5 ++
 .../dsv/scipro/forum/IndexedForumService.java | 12 ++++
 .../dsv/scipro/forum/ProjectForumService.java |  2 +
 .../scipro/forum/ProjectForumServiceImpl.java |  5 ++
 .../su/dsv/scipro/forum/ThreadOverview.java   | 15 ++++
 .../se/su/dsv/scipro/forum/Discussable.java   |  9 ---
 .../threaded/ForumThreadDiscussable.java      | 15 ----
 .../threaded/ProjectThreadedForumPage.java    | 17 ++---
 .../threaded/SupervisorThreadedForumPage.java | 22 ++----
 .../panels/threaded/ThreadReadStatePanel.java | 18 +++--
 .../panels/threaded/ThreadsOverviewPanel.java | 68 ++++++-------------
 .../su/dsv/scipro/group/AuthorGroupPage.java  | 22 ++----
 .../scipro/group/GroupThreadDiscussable.java  | 14 ----
 .../supervisor/pages/SupervisorGroupPage.java | 23 ++-----
 .../java/se/su/dsv/scipro/SciProTest.java     |  4 ++
 .../threaded/ThreadReadStatePanelTest.java    | 21 +++---
 .../threaded/ThreadsOverviewPanelTest.java    | 30 +++-----
 18 files changed, 109 insertions(+), 195 deletions(-)
 create mode 100644 core/src/main/java/se/su/dsv/scipro/forum/IndexedForumService.java
 create mode 100644 core/src/main/java/se/su/dsv/scipro/forum/ThreadOverview.java
 delete mode 100644 view/src/main/java/se/su/dsv/scipro/forum/Discussable.java
 delete mode 100644 view/src/main/java/se/su/dsv/scipro/forum/pages/threaded/ForumThreadDiscussable.java
 delete mode 100644 view/src/main/java/se/su/dsv/scipro/group/GroupThreadDiscussable.java

diff --git a/core/src/main/java/se/su/dsv/scipro/forum/GroupForumService.java b/core/src/main/java/se/su/dsv/scipro/forum/GroupForumService.java
index 5d43341f3c..158c241ee2 100644
--- a/core/src/main/java/se/su/dsv/scipro/forum/GroupForumService.java
+++ b/core/src/main/java/se/su/dsv/scipro/forum/GroupForumService.java
@@ -18,4 +18,6 @@ public interface GroupForumService {
     ForumPost createReply(GroupThread groupThread, User poster, String content, Set<Attachment> attachments);
 
     List<ForumPost> getPosts(GroupThread groupThread);
+
+    IndexedForumService getForumService(Group group);
 }
diff --git a/core/src/main/java/se/su/dsv/scipro/forum/GroupForumServiceImpl.java b/core/src/main/java/se/su/dsv/scipro/forum/GroupForumServiceImpl.java
index 00c48751cd..092fa0820b 100644
--- a/core/src/main/java/se/su/dsv/scipro/forum/GroupForumServiceImpl.java
+++ b/core/src/main/java/se/su/dsv/scipro/forum/GroupForumServiceImpl.java
@@ -81,4 +81,9 @@ public class GroupForumServiceImpl implements GroupForumService {
     public List<ForumPost> getPosts(final GroupThread groupThread) {
         return basicForumService.getPosts(groupThread.getForumThread());
     }
+
+    @Override
+    public IndexedForumService getForumService(Group group) {
+        return null;
+    }
 }
diff --git a/core/src/main/java/se/su/dsv/scipro/forum/IndexedForumService.java b/core/src/main/java/se/su/dsv/scipro/forum/IndexedForumService.java
new file mode 100644
index 0000000000..53c6b2c2fd
--- /dev/null
+++ b/core/src/main/java/se/su/dsv/scipro/forum/IndexedForumService.java
@@ -0,0 +1,12 @@
+package se.su.dsv.scipro.forum;
+
+import java.util.List;
+import se.su.dsv.scipro.system.User;
+
+public interface IndexedForumService {
+    List<ThreadOverview> getThreads();
+
+    boolean isThreadRead(User user, Long threadId);
+
+    void setThreadRead(User user, Long threadId, boolean read);
+}
diff --git a/core/src/main/java/se/su/dsv/scipro/forum/ProjectForumService.java b/core/src/main/java/se/su/dsv/scipro/forum/ProjectForumService.java
index 9269f37f49..0adc4a2402 100644
--- a/core/src/main/java/se/su/dsv/scipro/forum/ProjectForumService.java
+++ b/core/src/main/java/se/su/dsv/scipro/forum/ProjectForumService.java
@@ -28,4 +28,6 @@ public interface ProjectForumService {
     boolean canDelete(ProjectThread projectThread, ForumPost forumPost);
 
     void deletePost(ProjectThread projectThread, ForumPost forumPost);
+
+    IndexedForumService getForumService(Project project);
 }
diff --git a/core/src/main/java/se/su/dsv/scipro/forum/ProjectForumServiceImpl.java b/core/src/main/java/se/su/dsv/scipro/forum/ProjectForumServiceImpl.java
index 5850bffdc0..3c8bdbca4f 100644
--- a/core/src/main/java/se/su/dsv/scipro/forum/ProjectForumServiceImpl.java
+++ b/core/src/main/java/se/su/dsv/scipro/forum/ProjectForumServiceImpl.java
@@ -139,4 +139,9 @@ public class ProjectForumServiceImpl implements ProjectForumService {
     public ProjectThread findOne(long threadId) {
         return projectThreadRepository.findOne(threadId);
     }
+
+    @Override
+    public IndexedForumService getForumService(Project project) {
+        return null;
+    }
 }
diff --git a/core/src/main/java/se/su/dsv/scipro/forum/ThreadOverview.java b/core/src/main/java/se/su/dsv/scipro/forum/ThreadOverview.java
new file mode 100644
index 0000000000..ba73dddde2
--- /dev/null
+++ b/core/src/main/java/se/su/dsv/scipro/forum/ThreadOverview.java
@@ -0,0 +1,15 @@
+package se.su.dsv.scipro.forum;
+
+import java.time.Instant;
+import se.su.dsv.scipro.forum.dataobjects.ForumPost;
+import se.su.dsv.scipro.system.User;
+
+public record ThreadOverview(
+    Long id,
+    boolean hasAttachments,
+    String subject,
+    User author,
+    Instant createdAt,
+    int numberOfReplies,
+    ForumPost latestPost
+) {}
diff --git a/view/src/main/java/se/su/dsv/scipro/forum/Discussable.java b/view/src/main/java/se/su/dsv/scipro/forum/Discussable.java
deleted file mode 100644
index d6f69b8780..0000000000
--- a/view/src/main/java/se/su/dsv/scipro/forum/Discussable.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package se.su.dsv.scipro.forum;
-
-import org.apache.wicket.model.IModel;
-import org.apache.wicket.util.io.IClusterable;
-import se.su.dsv.scipro.forum.dataobjects.ForumThread;
-
-public interface Discussable<A> extends IClusterable {
-    IModel<ForumThread> discussion(IModel<A> a);
-}
diff --git a/view/src/main/java/se/su/dsv/scipro/forum/pages/threaded/ForumThreadDiscussable.java b/view/src/main/java/se/su/dsv/scipro/forum/pages/threaded/ForumThreadDiscussable.java
deleted file mode 100644
index 170bdddb5f..0000000000
--- a/view/src/main/java/se/su/dsv/scipro/forum/pages/threaded/ForumThreadDiscussable.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package se.su.dsv.scipro.forum.pages.threaded;
-
-import org.apache.wicket.model.IModel;
-import org.apache.wicket.model.LambdaModel;
-import se.su.dsv.scipro.forum.Discussable;
-import se.su.dsv.scipro.forum.dataobjects.ForumThread;
-import se.su.dsv.scipro.forum.dataobjects.ProjectThread;
-
-public class ForumThreadDiscussable implements Discussable<ProjectThread> {
-
-    @Override
-    public IModel<ForumThread> discussion(final IModel<ProjectThread> a) {
-        return LambdaModel.of(a, ProjectThread::getForumThread, ProjectThread::setForumThread);
-    }
-}
diff --git a/view/src/main/java/se/su/dsv/scipro/forum/pages/threaded/ProjectThreadedForumPage.java b/view/src/main/java/se/su/dsv/scipro/forum/pages/threaded/ProjectThreadedForumPage.java
index e42a52aaed..49083cdda8 100644
--- a/view/src/main/java/se/su/dsv/scipro/forum/pages/threaded/ProjectThreadedForumPage.java
+++ b/view/src/main/java/se/su/dsv/scipro/forum/pages/threaded/ProjectThreadedForumPage.java
@@ -11,6 +11,7 @@ import org.apache.wicket.model.LoadableDetachableModel;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
 import se.su.dsv.scipro.components.menuhighlighting.MenuHighlightAuthorMyProjects;
 import se.su.dsv.scipro.components.menuhighlighting.MenuHighlightForum;
+import se.su.dsv.scipro.forum.IndexedForumService;
 import se.su.dsv.scipro.forum.ProjectForumService;
 import se.su.dsv.scipro.forum.dataobjects.ForumThread;
 import se.su.dsv.scipro.forum.dataobjects.ProjectThread;
@@ -34,29 +35,19 @@ public class ProjectThreadedForumPage
     public ProjectThreadedForumPage(final PageParameters pp) {
         super(pp);
         add(new BookmarkablePageLink<Void>(CREATE_THREAD_LINK, ProjectCreateForumThreadPage.class, pp));
+        IModel<IndexedForumService> forumService = projectModel.map(projectForumService::getForumService);
         add(new FeedbackPanel(FEEDBACK));
         add(
-            new ThreadsOverviewPanel<>(FORUM, getThreads(), new ForumThreadDiscussable(), (id, thread) -> {
+            new ThreadsOverviewPanel(FORUM, forumService, (id, thread) -> {
                 PageParameters parameters = new PageParameters(pp).set(
                     PageParameterKeys.MAP.get(ForumThread.class),
-                    thread.getObject().getId()
+                    thread.getObject().id()
                 );
                 return new BookmarkablePageLink<>(id, ProjectViewForumThreadPage.class, parameters);
             })
         );
     }
 
-    private IModel<List<ProjectThread>> getThreads() {
-        return new LoadableDetachableModel<>() {
-            @Override
-            protected List<ProjectThread> load() {
-                List<ProjectThread> threads = projectForumService.getThreads(projectModel.getObject());
-                threads.sort(Collections.reverseOrder(Comparator.comparing(o -> o.getForumThread().getLastModified())));
-                return threads;
-            }
-        };
-    }
-
     static final String CREATE_THREAD_LINK = "createThread";
     static final String FEEDBACK = "feedback";
     static final String FORUM = "forum";
diff --git a/view/src/main/java/se/su/dsv/scipro/forum/pages/threaded/SupervisorThreadedForumPage.java b/view/src/main/java/se/su/dsv/scipro/forum/pages/threaded/SupervisorThreadedForumPage.java
index ffbf9664d9..41f82c8270 100644
--- a/view/src/main/java/se/su/dsv/scipro/forum/pages/threaded/SupervisorThreadedForumPage.java
+++ b/view/src/main/java/se/su/dsv/scipro/forum/pages/threaded/SupervisorThreadedForumPage.java
@@ -3,19 +3,15 @@ package se.su.dsv.scipro.forum.pages.threaded;
 import static se.su.dsv.scipro.security.auth.roles.Roles.SUPERVISOR;
 
 import jakarta.inject.Inject;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
 import org.apache.wicket.markup.html.link.BookmarkablePageLink;
 import org.apache.wicket.markup.html.panel.FeedbackPanel;
 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.MenuHighlightForum;
 import se.su.dsv.scipro.components.menuhighlighting.MenuHighlightSupervisorMyProjects;
+import se.su.dsv.scipro.forum.IndexedForumService;
 import se.su.dsv.scipro.forum.ProjectForumService;
 import se.su.dsv.scipro.forum.dataobjects.ForumThread;
-import se.su.dsv.scipro.forum.dataobjects.ProjectThread;
 import se.su.dsv.scipro.forum.panels.threaded.ThreadsOverviewPanel;
 import se.su.dsv.scipro.security.auth.Authorization;
 import se.su.dsv.scipro.security.auth.ProjectModuleComponent;
@@ -36,28 +32,18 @@ public class SupervisorThreadedForumPage
         super(pp);
         add(new BookmarkablePageLink<Void>(CREATE_THREAD_LINK, SupervisorCreateForumThreadPage.class, pp));
         add(new FeedbackPanel(FEEDBACK));
+        IModel<IndexedForumService> forumService = projectModel.map(projectForumService::getForumService);
         add(
-            new ThreadsOverviewPanel<>(FORUM, getThreads(), new ForumThreadDiscussable(), (id, thread) -> {
+            new ThreadsOverviewPanel(FORUM, forumService, (id, thread) -> {
                 PageParameters parameters = new PageParameters(pp).set(
                     PageParameterKeys.MAP.get(ForumThread.class),
-                    thread.getObject().getId()
+                    thread.getObject().id()
                 );
                 return new BookmarkablePageLink<>(id, SupervisorViewForumThreadPage.class, parameters);
             })
         );
     }
 
-    private IModel<List<ProjectThread>> getThreads() {
-        return new LoadableDetachableModel<>() {
-            @Override
-            protected List<ProjectThread> load() {
-                List<ProjectThread> threads = projectForumService.getThreads(projectModel.getObject());
-                threads.sort(Collections.reverseOrder(Comparator.comparing(o -> o.getForumThread().getLastModified())));
-                return threads;
-            }
-        };
-    }
-
     static final String CREATE_THREAD_LINK = "createThread";
     static final String FEEDBACK = "feedback";
     static final String FORUM = "forum";
diff --git a/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ThreadReadStatePanel.java b/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ThreadReadStatePanel.java
index 482b3e601c..422b39851b 100644
--- a/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ThreadReadStatePanel.java
+++ b/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ThreadReadStatePanel.java
@@ -1,10 +1,9 @@
 package se.su.dsv.scipro.forum.panels.threaded;
 
-import jakarta.inject.Inject;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.model.IModel;
-import se.su.dsv.scipro.forum.BasicForumService;
-import se.su.dsv.scipro.forum.dataobjects.ForumThread;
+import se.su.dsv.scipro.forum.IndexedForumService;
+import se.su.dsv.scipro.forum.ThreadOverview;
 import se.su.dsv.scipro.forum.panels.AbstractReadStatePanel;
 import se.su.dsv.scipro.session.SciProSession;
 
@@ -14,24 +13,23 @@ import se.su.dsv.scipro.session.SciProSession;
  */
 public class ThreadReadStatePanel extends AbstractReadStatePanel {
 
-    @Inject
-    private BasicForumService basicForumService;
+    private final IModel<IndexedForumService> forumService;
+    private final IModel<ThreadOverview> model;
 
-    private final IModel<ForumThread> model;
-
-    public ThreadReadStatePanel(String id, IModel<ForumThread> model) {
+    public ThreadReadStatePanel(String id, IModel<IndexedForumService> forumService, IModel<ThreadOverview> model) {
         super(id);
+        this.forumService = forumService;
         this.model = model;
     }
 
     @Override
     protected boolean isRead() {
-        return basicForumService.isThreadRead(SciProSession.get().getUser(), model.getObject());
+        return forumService.getObject().isThreadRead(SciProSession.get().getUser(), model.getObject().id());
     }
 
     @Override
     protected void onFlagClick(final AjaxRequestTarget target) {
         boolean read = isRead();
-        basicForumService.setThreadRead(SciProSession.get().getUser(), model.getObject(), !read);
+        forumService.getObject().setThreadRead(SciProSession.get().getUser(), model.getObject().id(), !read);
     }
 }
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 d0e7e1c55d..46dc4ae154 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,6 @@
 package se.su.dsv.scipro.forum.panels.threaded;
 
-import jakarta.inject.Inject;
 import java.io.Serializable;
-import java.util.List;
 import org.apache.wicket.markup.html.WebMarkupContainer;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.link.BookmarkablePageLink;
@@ -11,46 +9,39 @@ import org.apache.wicket.markup.html.list.ListView;
 import org.apache.wicket.markup.html.panel.Panel;
 import org.apache.wicket.model.IModel;
 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.IndexedForumService;
+import se.su.dsv.scipro.forum.ThreadOverview;
 import se.su.dsv.scipro.forum.dataobjects.ForumPost;
-import se.su.dsv.scipro.forum.dataobjects.ForumThread;
 import se.su.dsv.scipro.profile.UserLinkPanel;
 import se.su.dsv.scipro.system.User;
 
-public class ThreadsOverviewPanel<A> extends Panel {
-
-    @Inject
-    private BasicForumService basicForumService;
+public class ThreadsOverviewPanel extends Panel {
 
     public ThreadsOverviewPanel(
         final String id,
-        final IModel<List<A>> model,
-        final Discussable<A> discussable,
-        final ThreadLinkSupplier<A> threadLinkSupplier
+        final IModel<IndexedForumService> forumService,
+        final ThreadLinkSupplier threadLinkSupplier
     ) {
-        super(id, model);
+        super(id, forumService);
         add(
-            new ListView<>("threads", model) {
+            new ListView<>("threads", forumService.map(IndexedForumService::getThreads)) {
                 @Override
-                protected void populateItem(final ListItem<A> item) {
-                    final IModel<ForumThread> discussion = discussable.discussion(item.getModel());
-                    item.add(new ThreadReadStatePanel("readState", discussion));
+                protected void populateItem(final ListItem<ThreadOverview> item) {
+                    item.add(new ThreadReadStatePanel("readState", forumService, item.getModel()));
                     item.add(
                         new WebMarkupContainer("attachmentIcon") {
                             @Override
                             protected void onConfigure() {
                                 super.onConfigure();
-                                setVisibilityAllowed(basicForumService.hasAttachments(discussion.getObject()));
+                                setVisibilityAllowed(item.getModelObject().hasAttachments());
                             }
                         }
                     );
-                    item.add(new ThreadTitleColumn<>("threadTitle", discussion, item.getModel(), threadLinkSupplier));
-                    item.add(new Label("replies", discussion.map(ForumThread::getPostCount)));
-                    item.add(new LastPostColumn("lastPost", discussion));
+                    item.add(new ThreadTitleColumn<>("threadTitle", item.getModel(), threadLinkSupplier));
+                    item.add(new Label("replies", item.getModel().map(ThreadOverview::numberOfReplies)));
+                    item.add(new LastPostColumn("lastPost", item.getModel().map(ThreadOverview::latestPost)));
                 }
             }
         );
@@ -60,34 +51,26 @@ public class ThreadsOverviewPanel<A> extends Panel {
 
         public ThreadTitleColumn(
             final String id,
-            final IModel<ForumThread> thread,
-            final IModel<A> model,
-            final ThreadLinkSupplier<A> threadLinkSupplier
+            final IModel<ThreadOverview> model,
+            final ThreadLinkSupplier threadLinkSupplier
         ) {
             super(id);
             final BookmarkablePageLink<Void> threadLink = threadLinkSupplier.newThreadLink("threadLink", model);
-            threadLink.setBody(LambdaModel.of(thread, ForumThread::getSubject, ForumThread::setSubject));
+            threadLink.setBody(model.map(ThreadOverview::subject));
             add(threadLink);
-            add(new Label("createdBy", thread.map(ForumThread::getCreatedBy).map(User::getFullName).orElse("SciPro")));
-            add(
-                new DateLabel(
-                    "dateCreated",
-                    LambdaModel.of(thread, ForumThread::getDateCreated, ForumThread::setDateCreated),
-                    DateStyle.DATETIME
-                )
-            );
+            add(new Label("createdBy", model.map(ThreadOverview::author).map(User::getFullName).orElse("SciPro")));
+            add(new Label("dateCreated", model.map(ThreadOverview::createdAt)));
         }
     }
 
-    public interface ThreadLinkSupplier<A> extends Serializable {
-        BookmarkablePageLink<Void> newThreadLink(String id, IModel<A> thread);
+    public interface ThreadLinkSupplier extends Serializable {
+        BookmarkablePageLink<Void> newThreadLink(String id, IModel<ThreadOverview> thread);
     }
 
     private class LastPostColumn extends WebMarkupContainer {
 
-        public LastPostColumn(String id, final IModel<ForumThread> model) {
+        public LastPostColumn(String id, final IModel<ForumPost> postModel) {
             super(id);
-            IModel<ForumPost> postModel = getLastPost(model);
             add(
                 new UserLinkPanel("postedBy", LambdaModel.of(postModel, ForumPost::getPostedBy, ForumPost::setPostedBy))
             );
@@ -108,14 +91,5 @@ public class ThreadsOverviewPanel<A> extends Panel {
                 )
             );
         }
-
-        private IModel<ForumPost> getLastPost(final IModel<ForumThread> model) {
-            return new LoadableDetachableModel<>() {
-                @Override
-                protected ForumPost load() {
-                    return basicForumService.getLastPost(model.getObject());
-                }
-            };
-        }
     }
 }
diff --git a/view/src/main/java/se/su/dsv/scipro/group/AuthorGroupPage.java b/view/src/main/java/se/su/dsv/scipro/group/AuthorGroupPage.java
index cfcac3c98d..31ac2335c8 100644
--- a/view/src/main/java/se/su/dsv/scipro/group/AuthorGroupPage.java
+++ b/view/src/main/java/se/su/dsv/scipro/group/AuthorGroupPage.java
@@ -1,18 +1,14 @@
 package se.su.dsv.scipro.group;
 
 import jakarta.inject.Inject;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
 import org.apache.wicket.markup.html.link.BookmarkablePageLink;
 import org.apache.wicket.markup.html.panel.FeedbackPanel;
 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.MenuHighlightAuthorMyGroups;
 import se.su.dsv.scipro.forum.GroupForumService;
+import se.su.dsv.scipro.forum.IndexedForumService;
 import se.su.dsv.scipro.forum.dataobjects.ForumThread;
-import se.su.dsv.scipro.forum.dataobjects.GroupThread;
 import se.su.dsv.scipro.forum.panels.threaded.ThreadsOverviewPanel;
 import se.su.dsv.scipro.util.PageParameterKeys;
 
@@ -25,25 +21,15 @@ public class AuthorGroupPage extends AbstractAuthorGroupPage implements MenuHigh
         super(pp);
         add(new FeedbackPanel("feedback"));
         add(new BookmarkablePageLink<Void>("createThread", CreateThreadPage.class, pp));
+        IModel<IndexedForumService> forumService = getGroup().map(groupForumService::getForumService);
         add(
-            new ThreadsOverviewPanel<>("threads", getThreads(), new GroupThreadDiscussable(), (id, thread) -> {
+            new ThreadsOverviewPanel("threads", forumService, (id, thread) -> {
                 PageParameters parameters = new PageParameters(pp).set(
                     PageParameterKeys.MAP.get(ForumThread.class),
-                    thread.getObject().getId()
+                    thread.getObject().id()
                 );
                 return new BookmarkablePageLink<>(id, ViewThreadPage.class, parameters);
             })
         );
     }
-
-    private IModel<List<GroupThread>> getThreads() {
-        return new LoadableDetachableModel<>() {
-            @Override
-            protected List<GroupThread> load() {
-                List<GroupThread> threads = groupForumService.getThreads(getGroup().getObject());
-                threads.sort(Collections.reverseOrder(Comparator.comparing(o -> o.getForumThread().getLastModified())));
-                return threads;
-            }
-        };
-    }
 }
diff --git a/view/src/main/java/se/su/dsv/scipro/group/GroupThreadDiscussable.java b/view/src/main/java/se/su/dsv/scipro/group/GroupThreadDiscussable.java
deleted file mode 100644
index bbc44715b7..0000000000
--- a/view/src/main/java/se/su/dsv/scipro/group/GroupThreadDiscussable.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package se.su.dsv.scipro.group;
-
-import org.apache.wicket.model.IModel;
-import org.apache.wicket.model.LambdaModel;
-import se.su.dsv.scipro.forum.Discussable;
-import se.su.dsv.scipro.forum.dataobjects.*;
-
-public class GroupThreadDiscussable implements Discussable<GroupThread> {
-
-    @Override
-    public IModel<ForumThread> discussion(final IModel<GroupThread> a) {
-        return LambdaModel.of(a, GroupThread::getForumThread, GroupThread::setForumThread);
-    }
-}
diff --git a/view/src/main/java/se/su/dsv/scipro/supervisor/pages/SupervisorGroupPage.java b/view/src/main/java/se/su/dsv/scipro/supervisor/pages/SupervisorGroupPage.java
index 4307836a19..616c6e0b92 100644
--- a/view/src/main/java/se/su/dsv/scipro/supervisor/pages/SupervisorGroupPage.java
+++ b/view/src/main/java/se/su/dsv/scipro/supervisor/pages/SupervisorGroupPage.java
@@ -1,20 +1,15 @@
 package se.su.dsv.scipro.supervisor.pages;
 
 import jakarta.inject.Inject;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
 import org.apache.wicket.markup.html.link.BookmarkablePageLink;
 import org.apache.wicket.markup.html.panel.FeedbackPanel;
 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.forum.GroupForumService;
+import se.su.dsv.scipro.forum.IndexedForumService;
 import se.su.dsv.scipro.forum.dataobjects.ForumThread;
-import se.su.dsv.scipro.forum.dataobjects.GroupThread;
 import se.su.dsv.scipro.forum.panels.threaded.ThreadsOverviewPanel;
-import se.su.dsv.scipro.group.GroupThreadDiscussable;
 import se.su.dsv.scipro.util.PageParameterKeys;
 
 public class SupervisorGroupPage extends AbstractSupervisorGroupPage implements MenuHighlightSupervisorMyGroups {
@@ -26,25 +21,15 @@ public class SupervisorGroupPage extends AbstractSupervisorGroupPage implements
         super(pp);
         add(new FeedbackPanel("feedback"));
         add(new BookmarkablePageLink<Void>("createThread", SupervisorGroupCreateThreadPage.class, pp));
+        IModel<IndexedForumService> forumService = groupModel.map(groupForumService::getForumService);
         add(
-            new ThreadsOverviewPanel<>("threads", getThreads(), new GroupThreadDiscussable(), (id, thread) -> {
+            new ThreadsOverviewPanel("threads", forumService, (id, thread) -> {
                 PageParameters parameters = new PageParameters(pp).set(
                     PageParameterKeys.MAP.get(ForumThread.class),
-                    thread.getObject().getId()
+                    thread.getObject().id()
                 );
                 return new BookmarkablePageLink<>(id, SupervisorViewGroupThreadPage.class, parameters);
             })
         );
     }
-
-    private IModel<List<GroupThread>> getThreads() {
-        return new LoadableDetachableModel<>() {
-            @Override
-            protected List<GroupThread> load() {
-                List<GroupThread> threads = groupForumService.getThreads(groupModel.getObject());
-                threads.sort(Collections.reverseOrder(Comparator.comparing(o -> o.getForumThread().getLastModified())));
-                return threads;
-            }
-        };
-    }
 }
diff --git a/view/src/test/java/se/su/dsv/scipro/SciProTest.java b/view/src/test/java/se/su/dsv/scipro/SciProTest.java
index d83e739cb0..51be665c86 100755
--- a/view/src/test/java/se/su/dsv/scipro/SciProTest.java
+++ b/view/src/test/java/se/su/dsv/scipro/SciProTest.java
@@ -50,6 +50,7 @@ import se.su.dsv.scipro.finalthesis.FinalThesisService;
 import se.su.dsv.scipro.finalthesis.PublishingConsentService;
 import se.su.dsv.scipro.forum.BasicForumService;
 import se.su.dsv.scipro.forum.GroupForumService;
+import se.su.dsv.scipro.forum.IndexedForumService;
 import se.su.dsv.scipro.forum.ProjectForumService;
 import se.su.dsv.scipro.gdpr.Reporter;
 import se.su.dsv.scipro.generalsystemsettings.GeneralSystemSettings;
@@ -219,6 +220,9 @@ public abstract class SciProTest {
     @Mock
     protected GroupForumService groupForumService;
 
+    @Mock
+    protected IndexedForumService indexedForumService;
+
     @Mock
     protected GeneralSystemSettingsService generalSystemSettingsService;
 
diff --git a/view/src/test/java/se/su/dsv/scipro/forum/panels/threaded/ThreadReadStatePanelTest.java b/view/src/test/java/se/su/dsv/scipro/forum/panels/threaded/ThreadReadStatePanelTest.java
index 89f646f576..ffbeb764f3 100644
--- a/view/src/test/java/se/su/dsv/scipro/forum/panels/threaded/ThreadReadStatePanelTest.java
+++ b/view/src/test/java/se/su/dsv/scipro/forum/panels/threaded/ThreadReadStatePanelTest.java
@@ -4,22 +4,22 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.mockito.Mockito.*;
 
-import org.apache.wicket.model.Model;
+import java.time.Instant;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
 import se.su.dsv.scipro.SciProTest;
-import se.su.dsv.scipro.forum.dataobjects.ForumThread;
+import se.su.dsv.scipro.forum.ThreadOverview;
+import se.su.dsv.scipro.forum.dataobjects.ForumPost;
 import se.su.dsv.scipro.forum.panels.AbstractReadStatePanel;
 import se.su.dsv.scipro.system.User;
-import se.su.dsv.scipro.test.ForumBuilder;
 import se.su.dsv.scipro.test.UserBuilder;
 
 public class ThreadReadStatePanelTest extends SciProTest {
 
     ThreadReadStatePanel panel;
     User user;
-    ForumThread forumThread;
+    ThreadOverview forumThread;
 
     @BeforeEach
     public void setUp() throws Exception {
@@ -27,8 +27,7 @@ public class ThreadReadStatePanelTest extends SciProTest {
         setLoggedIn(true);
         setLoggedInAs(user);
 
-        forumThread = new ForumBuilder().createThread().getForumThread();
-        forumThread.addPost(new ForumBuilder().createPost(forumThread));
+        forumThread = new ThreadOverview(1L, false, "Subject", user, Instant.now(), 0, new ForumPost());
     }
 
     @Test
@@ -40,22 +39,24 @@ public class ThreadReadStatePanelTest extends SciProTest {
     @Test
     public void testTogglingCallsCorrectService() {
         // given
-        when(basicForumService.isThreadRead(eq(user), eq(forumThread))).thenReturn(true);
+        when(indexedForumService.isThreadRead(eq(user), eq(forumThread.id()))).thenReturn(true);
 
         // when
         startPanel();
         tester.executeAjaxEvent(path(panel.getId(), AbstractReadStatePanel.TOGGLE), "click");
 
-        verify(basicForumService, times(1)).setThreadRead(eq(user), eq(forumThread), eq(false));
+        verify(indexedForumService, times(1)).setThreadRead(eq(user), eq(forumThread.id()), eq(false));
 
         // then
         ArgumentCaptor<Boolean> captor = ArgumentCaptor.forClass(Boolean.class);
-        verify(basicForumService).setThreadRead(eq(user), eq(forumThread), captor.capture());
+        verify(indexedForumService).setThreadRead(eq(user), eq(forumThread.id()), captor.capture());
 
         assertEquals(false, captor.getValue(), "Does not toggle to the correct state");
     }
 
     private void startPanel() {
-        panel = tester.startComponentInPage(new ThreadReadStatePanel("panel", Model.of(forumThread)));
+        panel = tester.startComponentInPage(
+            new ThreadReadStatePanel("panel", () -> indexedForumService, () -> forumThread)
+        );
     }
 }
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 614f92e7cd..aad5c17b7d 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
@@ -1,33 +1,28 @@
 package se.su.dsv.scipro.forum.panels.threaded;
 
-import java.util.Arrays;
+import java.time.Instant;
 import java.util.List;
 import org.apache.wicket.markup.html.link.BookmarkablePageLink;
 import org.apache.wicket.markup.html.pages.InternalErrorPage;
-import org.apache.wicket.model.IModel;
-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;
+import se.su.dsv.scipro.forum.ThreadOverview;
 import se.su.dsv.scipro.forum.dataobjects.ForumPost;
-import se.su.dsv.scipro.forum.dataobjects.ForumThread;
 import se.su.dsv.scipro.system.User;
 
 @ExtendWith(MockitoExtension.class)
 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);
+        ThreadOverview forumThread = createThread();
+        Mockito.when(indexedForumService.getThreads()).thenReturn(List.of(forumThread));
     }
 
     @Test
@@ -36,17 +31,10 @@ public class ThreadsOverviewPanelTest extends SciProTest {
     }
 
     private void startPanel() {
-        IModel<List<ForumThread>> listIModel = Model.ofList(threads);
         tester.startComponentInPage(
-            new ThreadsOverviewPanel<>(
+            new ThreadsOverviewPanel(
                 "id",
-                listIModel,
-                new Discussable<ForumThread>() {
-                    @Override
-                    public IModel<ForumThread> discussion(final IModel<ForumThread> a) {
-                        return a;
-                    }
-                },
+                () -> indexedForumService,
                 (id, thread) -> {
                     final BookmarkablePageLink<Void> link = new BookmarkablePageLink<>(id, InternalErrorPage.class);
                     return link;
@@ -55,12 +43,10 @@ public class ThreadsOverviewPanelTest extends SciProTest {
         );
     }
 
-    private ForumThread createThread() {
+    private ThreadOverview createThread() {
         User bob = User.builder().firstName("Bob").lastName("the Builder").emailAddress("bob@building.com").build();
         post = new ForumPost();
         post.setPostedBy(bob);
-        ForumThread groupForumThread = new ForumThread();
-        groupForumThread.addPost(post);
-        return groupForumThread;
+        return new ThreadOverview(1L, true, "Subject", bob, Instant.now(), 1, post);
     }
 }