From e84d256681930d86b01b7de7ff0d58d58bd699b3 Mon Sep 17 00:00:00 2001
From: uhensler <urs.hensler@frentix.com>
Date: Mon, 16 Sep 2019 08:36:55 +0200
Subject: [PATCH] OO-4168: New course tool: forum

---
 .../core/logging/activity/ActionObject.java   |   1 +
 .../LearningResourceLoggingAction.java        |   4 +
 .../org/olat/course/config/CourseConfig.java  |  33 ++-
 .../olat/course/config/CourseConfigEvent.java |   1 +
 .../config/ui/CourseToolbarController.java    |  26 +++
 .../ui/_i18n/LocalStrings_de.properties       |   1 +
 .../ui/_i18n/LocalStrings_en.properties       |   1 +
 .../course/nodes/fo/FOToolController.java     | 211 ++++++++++++++++++
 .../course/run/CourseRuntimeController.java   |  46 +++-
 .../run/_i18n/LocalStrings_de.properties      |   1 +
 .../run/_i18n/LocalStrings_en.properties      |   1 +
 src/main/java/org/olat/modules/fo/Forum.java  |   5 +-
 .../olat/modules/fo/manager/ForumManager.java |  17 +-
 .../org/olat/modules/fo/model/ForumImpl.java  |  33 +++
 .../RepositoryEntryManagedFlag.java           |   1 +
 .../database/mysql/alter_14_2_x_to_15_0_0.sql |   5 +
 .../database/mysql/setupDatabase.sql          |   3 +
 .../oracle/alter_14_2_x_to_15_0_0.sql         |   6 +
 .../database/oracle/setupDatabase.sql         |   3 +
 .../postgresql/alter_14_2_x_to_15_0_0.sql     |   5 +
 .../database/postgresql/setupDatabase.sql     |   3 +
 .../org/olat/modules/fo/ForumManagerTest.java |  22 ++
 22 files changed, 424 insertions(+), 5 deletions(-)
 create mode 100644 src/main/java/org/olat/course/nodes/fo/FOToolController.java

diff --git a/src/main/java/org/olat/core/logging/activity/ActionObject.java b/src/main/java/org/olat/core/logging/activity/ActionObject.java
index b948478af4c..62c820b77fd 100644
--- a/src/main/java/org/olat/core/logging/activity/ActionObject.java
+++ b/src/main/java/org/olat/core/logging/activity/ActionObject.java
@@ -55,6 +55,7 @@ public enum ActionObject {
 	feeditem,
 	file,
 	folder,
+	forum,
 	forummessage,
 	forumthread,
 	glossar,
diff --git a/src/main/java/org/olat/core/logging/activity/LearningResourceLoggingAction.java b/src/main/java/org/olat/core/logging/activity/LearningResourceLoggingAction.java
index ba5d8cbb3f3..9725031a9d2 100644
--- a/src/main/java/org/olat/core/logging/activity/LearningResourceLoggingAction.java
+++ b/src/main/java/org/olat/core/logging/activity/LearningResourceLoggingAction.java
@@ -107,6 +107,10 @@ public class LearningResourceLoggingAction extends BaseLoggingAction {
 			new LearningResourceLoggingAction(ActionType.admin, CrudAction.update, ActionVerb.add, ActionObject.mail).setTypeList(LEARNING_RESOURCE_OPEN_CLOSE_LIST);
 	public static final ILoggingAction REPOSITORY_ENTRY_PROPERTIES_EMAIL_DISABLED = 
 			new LearningResourceLoggingAction(ActionType.admin, CrudAction.update, ActionVerb.remove, ActionObject.mail).setTypeList(LEARNING_RESOURCE_OPEN_CLOSE_LIST);
+	public static final ILoggingAction REPOSITORY_ENTRY_PROPERTIES_FORUM_ENABLED = 
+			new LearningResourceLoggingAction(ActionType.admin, CrudAction.update, ActionVerb.add, ActionObject.forum).setTypeList(LEARNING_RESOURCE_OPEN_CLOSE_LIST);
+	public static final ILoggingAction REPOSITORY_ENTRY_PROPERTIES_FORUM_DISABLED = 
+			new LearningResourceLoggingAction(ActionType.admin, CrudAction.update, ActionVerb.remove, ActionObject.forum).setTypeList(LEARNING_RESOURCE_OPEN_CLOSE_LIST);
 	public static final ILoggingAction REPOSITORY_ENTRY_PROPERTIES_GLOSSARY_ENABLED = 
 		new LearningResourceLoggingAction(ActionType.admin, CrudAction.update, ActionVerb.add, ActionObject.glossar).setTypeList(LEARNING_RESOURCE_OPEN_CLOSE_LIST);
 	public static final ILoggingAction REPOSITORY_ENTRY_PROPERTIES_GLOSSARY_DISABLED = 
diff --git a/src/main/java/org/olat/course/config/CourseConfig.java b/src/main/java/org/olat/course/config/CourseConfig.java
index 40e16246b07..b0245530be8 100644
--- a/src/main/java/org/olat/course/config/CourseConfig.java
+++ b/src/main/java/org/olat/course/config/CourseConfig.java
@@ -74,7 +74,7 @@ public class CourseConfig implements Serializable, Cloneable {
 	/**
 	 * current config file version
 	 */
-	private static final transient int CURRENTVERSION = 16;
+	private static final transient int CURRENTVERSION = 17;
 	
 	public static final transient String KEY_LOGLEVEL_ADMIN = "LOGLEVELADMIN";
 	public static final transient String KEY_LOGLEVEL_USER = "LOGLEVELUSER";
@@ -103,6 +103,8 @@ public class CourseConfig implements Serializable, Cloneable {
 	public static final transient String PARTICIPANT_LIST_ENABLED = "PARTICIPANT_LIST_ENABLED";
 	public static final transient String PARTICIPANT_INFO_ENABLED = "PARTICIPANT_INFO_ENABLED";
 	public static final transient String EMAIL_ENABLED = "EMAIL_ENABLED";
+	public static final transient String FORUM_ENABLED = "FORUM_ENABLED";
+	public static final transient String DOCUMENTS_ENABLED = "DOCUMENTS_ENABLED";
 	public static final transient String KEY_CALENDAR_ENABLED = "KEY_CALENDAR_ENABLED";
 	
 	public static final transient String KEY_GLOSSARY_ENABLED = "KEY_GLOSSARY_ENABLED";
@@ -169,6 +171,8 @@ public class CourseConfig implements Serializable, Cloneable {
 		configuration.put(PARTICIPANT_LIST_ENABLED, Boolean.FALSE);
 		configuration.put(PARTICIPANT_INFO_ENABLED, Boolean.FALSE);
 		configuration.put(EMAIL_ENABLED, Boolean.FALSE);
+		configuration.put(FORUM_ENABLED, Boolean.FALSE);
+		configuration.put(DOCUMENTS_ENABLED, Boolean.FALSE);
 		
 		configuration.put(NODE_ACCESS_TYPE, NODE_ACCESS_TYPE_DEFAULT);
 
@@ -277,6 +281,13 @@ public class CourseConfig implements Serializable, Cloneable {
 				
 				this.version = 16;
 			}
+			
+			if (version == 17) {
+				if (!configuration.containsKey(FORUM_ENABLED)) configuration.put(FORUM_ENABLED, Boolean.FALSE);
+				if (!configuration.containsKey(DOCUMENTS_ENABLED)) configuration.put(FORUM_ENABLED, Boolean.FALSE);
+				
+				this.version = 17;
+			}
 
 			
 			/*
@@ -584,6 +595,24 @@ public class CourseConfig implements Serializable, Cloneable {
 		configuration.put(EMAIL_ENABLED, Boolean.valueOf(b));
 	}
 	
+	public boolean isForumEnabled() {
+		Boolean bool = (Boolean) configuration.get(FORUM_ENABLED);
+		return bool.booleanValue();
+	}
+	
+	public void setForumEnabled(boolean b) {
+		configuration.put(FORUM_ENABLED, Boolean.valueOf(b));
+	}
+	
+	public boolean isDocumentsEnabled() {
+		Boolean bool = (Boolean) configuration.get(DOCUMENTS_ENABLED);
+		return bool.booleanValue();
+	}
+	
+	public void setDocumentsEnabled(boolean b) {
+		configuration.put(DOCUMENTS_ENABLED, Boolean.valueOf(b));
+	}
+	
 	public boolean isToolbarEnabled() {
 		Boolean bool = (Boolean) configuration.get(TOOLBAR_ENABLED);
 		return bool.booleanValue();
@@ -629,6 +658,8 @@ public class CourseConfig implements Serializable, Cloneable {
 		clone.setParticipantListEnabled(isParticipantListEnabled());
 		clone.setParticipantInfoEnabled(isParticipantInfoEnabled());
 		clone.setEmailEnabled(isEmailEnabled());
+		clone.setForumEnabled(isForumEnabled());
+		clone.setDocumentsEnabled(isDocumentsEnabled());
 		return clone;
 	}
 
diff --git a/src/main/java/org/olat/course/config/CourseConfigEvent.java b/src/main/java/org/olat/course/config/CourseConfigEvent.java
index 9265caad496..55e52b0cae0 100644
--- a/src/main/java/org/olat/course/config/CourseConfigEvent.java
+++ b/src/main/java/org/olat/course/config/CourseConfigEvent.java
@@ -63,6 +63,7 @@ public class CourseConfigEvent extends MultiUserEvent {
 		participantList,
 		participantInfo,
 		email,
+		forum,
 		chat,
 		glossary,
 		layout
diff --git a/src/main/java/org/olat/course/config/ui/CourseToolbarController.java b/src/main/java/org/olat/course/config/ui/CourseToolbarController.java
index 2e7b52cc514..300b2d8c4f6 100644
--- a/src/main/java/org/olat/course/config/ui/CourseToolbarController.java
+++ b/src/main/java/org/olat/course/config/ui/CourseToolbarController.java
@@ -73,6 +73,7 @@ public class CourseToolbarController extends FormBasicController {
 	private SelectionElement participantListEl;
 	private SelectionElement participantInfoEl;
 	private SelectionElement emailEl;
+	private SelectionElement forumEl;
 	private SelectionElement chatEl;
 	private SelectionElement glossaryEl;
 	
@@ -180,6 +181,15 @@ public class CourseToolbarController extends FormBasicController {
 			canHideToolbar &= false;
 		}
 		
+		boolean forumEnabled = courseConfig.isForumEnabled();
+		boolean managedForum = RepositoryEntryManagedFlag.isManaged(entry, RepositoryEntryManagedFlag.forum);
+		forumEl = uifactory.addCheckboxesHorizontal("forumIsOn", "chkbx.forum.onoff", formLayout, onKeys, onValues);
+		forumEl.select(onKeys[0], forumEnabled);
+		forumEl.setEnabled(editable && !managedEmail);
+		if(managedForum && forumEnabled) {
+			canHideToolbar &= false;
+		}
+		
 		boolean chatEnabled = courseConfig.isChatEnabled();
 		boolean managedChat = RepositoryEntryManagedFlag.isManaged(entry, RepositoryEntryManagedFlag.chat);
 		chatEl = uifactory.addCheckboxesHorizontal("chatIsOn", "chkbx.chat.onoff", formLayout, onKeys, onValues);
@@ -227,6 +237,7 @@ public class CourseToolbarController extends FormBasicController {
 				|| participantListEl.isSelected(0)
 				|| participantInfoEl.isSelected(0)
 				|| emailEl.isSelected(0)
+				|| forumEl.isSelected(0)
 				|| chatEl.isSelected(0)
 				|| glossaryEl.isSelected(0);
 	}
@@ -241,6 +252,7 @@ public class CourseToolbarController extends FormBasicController {
 		participantListEl.setVisible(enabled);
 		participantInfoEl.setVisible(enabled);
 		emailEl.setVisible(enabled);
+		forumEl.setVisible(enabled);
 		chatEl.setVisible(enabled);
 		glossaryEl.setVisible(enabled);
 	}
@@ -274,6 +286,10 @@ public class CourseToolbarController extends FormBasicController {
 		boolean updateEmail = courseConfig.isEmailEnabled() != enableEmail;
 		courseConfig.setEmailEnabled(enableEmail && toolbarEnabled);
 		
+		boolean enableForum = forumEl.isSelected(0);
+		boolean updateForum = courseConfig.isForumEnabled() != enableForum;
+		courseConfig.setForumEnabled(enableForum && toolbarEnabled);
+		
 		boolean enableChat = chatEl.isSelected(0);
 		boolean updateChat = courseConfig.isChatEnabled() != enableChat;
 		courseConfig.setChatIsEnabled(enableChat && toolbarEnabled);
@@ -337,6 +353,16 @@ public class CourseToolbarController extends FormBasicController {
 				.fireEventToListenersOf(new CourseConfigEvent(CourseConfigType.email, course.getResourceableId()), course);
 		}
 		
+		if(updateForum) {
+			ILoggingAction loggingAction = enableForum ?
+					LearningResourceLoggingAction.REPOSITORY_ENTRY_PROPERTIES_FORUM_ENABLED:
+					LearningResourceLoggingAction.REPOSITORY_ENTRY_PROPERTIES_FORUM_DISABLED;
+			ThreadLocalUserActivityLogger.log(loggingAction, getClass());
+			
+			CoordinatorManager.getInstance().getCoordinator().getEventBus()
+				.fireEventToListenersOf(new CourseConfigEvent(CourseConfigType.forum, course.getResourceableId()), course);
+		}
+		
 		if(updateChat) {
 			ILoggingAction loggingAction =enableChat ?
 					LearningResourceLoggingAction.REPOSITORY_ENTRY_PROPERTIES_IM_ENABLED:
diff --git a/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_de.properties
index c69e1b4036d..08d41f9b4cb 100644
--- a/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_de.properties
@@ -3,6 +3,7 @@ chkbx.calendar.onoff=Kurskalender
 chkbx.chat.onoff=Kurs-Chat
 chkbx.efficency.onoff=Leistungsnachweis verwenden
 chkbx.email.onoff=E-Mail
+chkbx.forum.onoff=Forum
 chkbx.glossary.explain=Das Glossar muss unter "Optionen" konfiguriert werden.
 chkbx.glossary.inverse.explain=Glossar Menu in Toolbar muss unter "Toolbar" konfiguriert werden.
 chkbx.glossary.onoff=Glossar
diff --git a/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_en.properties
index 6007cec9805..820d771b413 100644
--- a/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_en.properties
@@ -3,6 +3,7 @@ chkbx.calendar.onoff=Course calendar
 chkbx.chat.onoff=Course chat
 chkbx.search.onoff=Course search
 chkbx.email.onoff=E-mail
+chkbx.forum.onoff=Forum
 chkbx.efficency.onoff=Use evidence of achievement
 chkbx.glossary.explain=The glossary need to be configured under "Options".
 chkbx.glossary.inverse.explain=Glossary menu in toolbar is configured under "Toolbar".
diff --git a/src/main/java/org/olat/course/nodes/fo/FOToolController.java b/src/main/java/org/olat/course/nodes/fo/FOToolController.java
new file mode 100644
index 00000000000..2aaf6325be0
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/fo/FOToolController.java
@@ -0,0 +1,211 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.course.nodes.fo;
+
+import org.olat.core.commons.services.notifications.SubscriptionContext;
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.Component;
+import org.olat.core.gui.control.Event;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.gui.control.controller.BasicController;
+import org.olat.course.run.userview.UserCourseEnvironment;
+import org.olat.modules.fo.Forum;
+import org.olat.modules.fo.ForumCallback;
+import org.olat.modules.fo.manager.ForumManager;
+import org.olat.modules.fo.ui.ForumController;
+import org.olat.repository.RepositoryEntry;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 13 Sep 2019<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public class FOToolController extends BasicController {
+	
+	private static final String SUBSCRIPTION_SUBIDENTIFIER = "forum";
+
+	private final ForumController forumCtrl;
+
+	@Autowired
+	private ForumManager forumManager;
+
+	public FOToolController(UserRequest ureq, WindowControl wControl, UserCourseEnvironment userCourseEnv) {
+		super(ureq, wControl);
+		
+		RepositoryEntry courseEntry = userCourseEnv.getCourseEnvironment().getCourseGroupManager().getCourseEntry();
+		Forum forum = getOrCreateForum(courseEntry);
+		
+	
+		SubscriptionContext forumSubContext = new SubscriptionContext(courseEntry, SUBSCRIPTION_SUBIDENTIFIER);
+		ForumCallback forumCallback = userCourseEnv.isCourseReadOnly() || ureq.getUserSession().getRoles().isGuestOnly()
+				? new ReadOnlyForumCallback(userCourseEnv)
+				: new ToolSecurityCallback(userCourseEnv, forumSubContext);
+				
+		forumCtrl = new ForumController(ureq, wControl, forum, forumCallback, true);
+		listenTo(forumCtrl);
+		putInitialPanel(forumCtrl.getInitialComponent());
+	}
+
+	private Forum getOrCreateForum(RepositoryEntry courseEntry) {
+		Forum forum = forumManager.loadForum(courseEntry);
+		if (forum == null) {
+			forum = forumManager.addAForum(courseEntry);
+		}
+		return forum;
+	}
+
+	@Override
+	protected void event(UserRequest ureq, Component source, Event event) {
+		//
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+	
+	private static class ToolSecurityCallback implements ForumCallback {
+		
+		private final boolean admin;
+		private final boolean coach;
+		private final SubscriptionContext subscriptionContext;
+
+		private ToolSecurityCallback(UserCourseEnvironment userCourseEnv, SubscriptionContext subscriptionContext) {
+			this.admin = userCourseEnv.isAdmin();
+			this.coach = userCourseEnv.isCoach();
+			this.subscriptionContext = subscriptionContext;
+		}
+
+		@Override
+		public boolean mayUsePseudonym() {
+			return false;
+		}
+
+		@Override
+		public boolean mayOpenNewThread() {
+			return true;
+		}
+
+		@Override
+		public boolean mayReplyMessage() {
+			return true;
+		}
+
+		@Override
+		public boolean mayEditOwnMessage() {
+			return true;
+		}
+
+		@Override
+		public boolean mayDeleteOwnMessage() {
+			return true;
+		}
+
+		@Override
+		public boolean mayEditMessageAsModerator() {
+			return admin || coach;
+		}
+
+		@Override
+		public boolean mayDeleteMessageAsModerator() {
+			return admin || coach;
+		}
+
+		@Override
+		public boolean mayArchiveForum() {
+			return false;
+		}
+
+		@Override
+		public boolean mayFilterForUser() {
+			return admin || coach;
+		}
+
+		@Override
+		public SubscriptionContext getSubscriptionContext() {
+			return subscriptionContext;
+		}
+		
+	}
+	
+	private static class ReadOnlyForumCallback implements ForumCallback {
+		
+		private final boolean admin;
+		private final boolean coach;
+		
+		public ReadOnlyForumCallback(UserCourseEnvironment userCourseEnv) {
+			this.admin = userCourseEnv.isAdmin();
+			this.coach = userCourseEnv.isCoach();
+		}
+
+		@Override
+		public boolean mayUsePseudonym() {
+			return false;
+		}
+
+		@Override
+		public boolean mayOpenNewThread() {
+			return false;
+		}
+
+		@Override
+		public boolean mayReplyMessage() {
+			return false;
+		}
+		
+		@Override
+		public boolean mayEditOwnMessage() {
+			return false;
+		}
+
+		@Override
+		public boolean mayDeleteOwnMessage() {
+			return false;
+		}
+
+		@Override
+		public boolean mayEditMessageAsModerator() {
+			return false;
+		}
+
+		@Override
+		public boolean mayDeleteMessageAsModerator() {
+			return false;
+		}
+
+		@Override
+		public boolean mayArchiveForum() {
+			return false;
+		}
+
+		@Override
+		public boolean mayFilterForUser() {
+			return admin || coach;
+		}
+
+		@Override
+		public SubscriptionContext getSubscriptionContext() {
+			return null;
+		}
+	}
+
+}
diff --git a/src/main/java/org/olat/course/run/CourseRuntimeController.java b/src/main/java/org/olat/course/run/CourseRuntimeController.java
index e227298964a..dba0e1fc6dc 100644
--- a/src/main/java/org/olat/course/run/CourseRuntimeController.java
+++ b/src/main/java/org/olat/course/run/CourseRuntimeController.java
@@ -106,6 +106,7 @@ import org.olat.course.member.MembersManagementMainController;
 import org.olat.course.nodes.CourseNode;
 import org.olat.course.nodes.ENCourseNode;
 import org.olat.course.nodes.co.COToolController;
+import org.olat.course.nodes.fo.FOToolController;
 import org.olat.course.nodes.info.InfoRunController;
 import org.olat.course.nodes.members.MembersToolRunController;
 import org.olat.course.reminder.ui.CourseRemindersController;
@@ -178,7 +179,8 @@ public class CourseRuntimeController extends RepositoryEntryRuntimeController im
 		//my course
 		efficiencyStatementsLink, noteLink, leaveLink,
 		//course tools
-		learninPathLink, calendarLink, chatLink, participantListLink, participantInfoLink, emailLink, searchLink,
+		learninPathLink, calendarLink, chatLink, participantListLink, participantInfoLink, forumLink, emailLink,
+		searchLink,
 		//glossary
 		openGlossaryLink, enableGlossaryLink, lecturesLink;
 	private Link currentUserCountLink;
@@ -186,6 +188,7 @@ public class CourseRuntimeController extends RepositoryEntryRuntimeController im
 
 	private CloseableModalController cmc;
 	private COToolController emailCtrl;
+	private FOToolController forumCtrl;
 	private CourseAreasController areasCtrl;
 	private ConfirmLeaveController leaveDialogBox;
 	private ArchiverMainController archiverCtrl;
@@ -830,6 +833,12 @@ public class CourseRuntimeController extends RepositoryEntryRuntimeController im
 			toolbarPanel.addTool(emailLink);
 		}
 		
+		if(!assessmentLock) {
+			forumLink = LinkFactory.createToolLink("forum", translate("command.forum"), this, "o_fo_icon");
+			forumLink.setVisible(cc.isForumEnabled());
+			toolbarPanel.addTool(forumLink);
+		}
+		
 		if(!assessmentLock) {
 			glossary = new Dropdown("glossary", "command.glossary", false, getTranslator());
 			glossary.setIconCSS("o_icon o_FileResource-GLOSSARY_icon");
@@ -962,6 +971,8 @@ public class CourseRuntimeController extends RepositoryEntryRuntimeController im
 			doParticipantInfo(ureq);
 		} else if(emailLink == source) {
 			doEmail(ureq);
+		} else if(forumLink == source) {
+			doForum(ureq);
 		} else if(learninPathLink == source) {
 			doLearningPath(ureq);
 		} else if(calendarLink == source) {
@@ -1069,6 +1080,7 @@ public class CourseRuntimeController extends RepositoryEntryRuntimeController im
 						case participantList: doParticipantList(ureq); break;
 						case participantInfo: doParticipantInfo(ureq); break;
 						case email: doEmail(ureq); break;
+						case forum: doForum(ureq); break;
 					}
 					delayedClose = null;
 				} else {
@@ -1151,6 +1163,10 @@ public class CourseRuntimeController extends RepositoryEntryRuntimeController im
 				if (emailLink != null && emailLink.isVisible()) {
 					doEmail(ureq);
 				}
+			} else if("Forum".equalsIgnoreCase(type)) {
+				if (forumLink != null && forumLink.isVisible()) {
+					doForum(ureq);
+				}
 			} else if("Certification".equalsIgnoreCase(type)) {
 				doEfficiencyStatements(ureq);
 			} else if("Reminders".equalsIgnoreCase(type) || "RemindersLogs".equalsIgnoreCase(type)) {
@@ -1760,6 +1776,22 @@ public class CourseRuntimeController extends RepositoryEntryRuntimeController im
 		};
 	}
 	
+	private void doForum(UserRequest ureq) {
+		if(delayedClose == Delayed.forum || requestForClose(ureq)) {
+			removeCustomCSS();
+			
+			OLATResourceable ores = OresHelper.createOLATResourceableType("forum");
+			WindowControl swControl = addToHistory(ureq, ores, null);
+			forumCtrl = new FOToolController(ureq, swControl, getUserCourseEnvironment());
+
+			pushController(ureq, translate("command.forum"), forumCtrl);
+			setActiveTool(forumLink);
+			currentToolCtr = forumCtrl;
+		} else {
+			delayedClose = Delayed.forum;
+		};
+	}
+	
 	private void launchCalendar(UserRequest ureq) {
 		ControllerCreator ctrlCreator = (lureq, lwControl) -> {
 			ICourse course = CourseFactory.loadCourse(getRepositoryEntry());
@@ -1912,6 +1944,15 @@ public class CourseRuntimeController extends RepositoryEntryRuntimeController im
 				}
 				break;
 			}
+			case forum: {
+				if(forumLink != null) {
+					ICourse course = CourseFactory.loadCourse(getRepositoryEntry());
+					CourseConfig cc = course.getCourseEnvironment().getCourseConfig();
+					forumLink.setVisible(cc.isForumEnabled());
+					toolbarPanel.setDirty(true);
+				}
+				break;
+			}
 			case chat: {
 				if(chatLink != null) {
 					ICourse course = CourseFactory.loadCourse(getRepositoryEntry());
@@ -2052,6 +2093,7 @@ public class CourseRuntimeController extends RepositoryEntryRuntimeController im
 		pop,
 		participantList,
 		participantInfo,
-		email
+		email,
+		forum
 	}
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/run/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/run/_i18n/LocalStrings_de.properties
index 0dfba24b955..e61d9057560 100644
--- a/src/main/java/org/olat/course/run/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/course/run/_i18n/LocalStrings_de.properties
@@ -14,6 +14,7 @@ command.coursefolder=Ablageordner
 command.coursesearch=Kurssuche
 command.efficiencystatement=Leistungsnachweis
 command.email=E-Mail
+command.forum=Forum
 command.glossary=Glossar
 command.glossary.open=Glossar in separatem Fenster \u00F6ffnen
 command.glossary.off=aus
diff --git a/src/main/java/org/olat/course/run/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/course/run/_i18n/LocalStrings_en.properties
index e70f42d6494..d46f5e276ef 100644
--- a/src/main/java/org/olat/course/run/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/course/run/_i18n/LocalStrings_en.properties
@@ -14,6 +14,7 @@ command.coursefolder=Storage folder
 command.coursesearch=Course search
 command.efficiencystatement=Evidence of achievement
 command.email=E-Mail
+command.forum=Forum
 command.glossary=Glossary
 command.glossary.off=off
 command.glossary.off.alt=Hide glossary terms of learning content
diff --git a/src/main/java/org/olat/modules/fo/Forum.java b/src/main/java/org/olat/modules/fo/Forum.java
index 5590cf35d85..be748b5a2a6 100644
--- a/src/main/java/org/olat/modules/fo/Forum.java
+++ b/src/main/java/org/olat/modules/fo/Forum.java
@@ -35,5 +35,8 @@ import org.olat.core.id.Persistable;
  * @author Felix Jost
  */
 public interface Forum extends CreateInfo, Persistable, OLATResourceable {
-    // nothing to be declared
+	
+	public OLATResourceable getReference();
+	
+	public void setReference(OLATResourceable ores);
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/modules/fo/manager/ForumManager.java b/src/main/java/org/olat/modules/fo/manager/ForumManager.java
index 9efe68af6d2..d43e87ce375 100644
--- a/src/main/java/org/olat/modules/fo/manager/ForumManager.java
+++ b/src/main/java/org/olat/modules/fo/manager/ForumManager.java
@@ -40,6 +40,7 @@ import java.util.Set;
 import javax.persistence.TemporalType;
 import javax.persistence.TypedQuery;
 
+import org.apache.logging.log4j.Logger;
 import org.olat.basesecurity.IdentityRef;
 import org.olat.core.commons.modules.bc.FolderConfig;
 import org.olat.core.commons.persistence.DB;
@@ -50,7 +51,6 @@ import org.olat.core.commons.services.text.TextService;
 import org.olat.core.id.Identity;
 import org.olat.core.id.OLATResourceable;
 import org.olat.core.logging.AssertException;
-import org.apache.logging.log4j.Logger;
 import org.olat.core.logging.Tracing;
 import org.olat.core.util.Encoder;
 import org.olat.core.util.Encoder.Algorithm;
@@ -706,8 +706,13 @@ public class ForumManager {
 	 * @return the newly created and persisted forum
 	 */
 	public Forum addAForum() {
+		return addAForum(null);
+	}
+
+	public Forum addAForum(OLATResourceable refrence) {
 		ForumImpl fo = new ForumImpl();
 		fo.setCreationDate(new Date());
+		fo.setReference(refrence);
 		dbInstance.getCurrentEntityManager().persist(fo);
 		return fo;
 	}
@@ -724,6 +729,16 @@ public class ForumManager {
 				.getResultList();
 		return forumList == null || forumList.isEmpty() ? null : forumList.get(0);
 	}
+	
+	public Forum loadForum(OLATResourceable refrence) {
+		String q = "select fo from forum as fo where fo.refResName=:refResName and fo.refResId=:refResId";
+		List<Forum> forumList = dbInstance.getCurrentEntityManager()
+				.createQuery(q, Forum.class)
+				.setParameter("refResName", refrence.getResourceableTypeName())
+				.setParameter("refResId", refrence.getResourceableId())
+				.getResultList();
+		return forumList == null || forumList.isEmpty() ? null : forumList.get(0);
+	}
 
 	/**
 	 * @param forumKey
diff --git a/src/main/java/org/olat/modules/fo/model/ForumImpl.java b/src/main/java/org/olat/modules/fo/model/ForumImpl.java
index 11635603b6a..55754a98f0c 100644
--- a/src/main/java/org/olat/modules/fo/model/ForumImpl.java
+++ b/src/main/java/org/olat/modules/fo/model/ForumImpl.java
@@ -39,6 +39,7 @@ import javax.persistence.Version;
 import org.hibernate.annotations.GenericGenerator;
 import org.hibernate.annotations.Parameter;
 import org.olat.core.id.CreateInfo;
+import org.olat.core.id.OLATResourceable;
 import org.olat.core.id.Persistable;
 import org.olat.core.logging.AssertException;
 import org.olat.core.util.resource.OresHelper;
@@ -74,6 +75,11 @@ public class ForumImpl implements Forum, CreateInfo, Persistable{
 	@Temporal(TemporalType.TIMESTAMP)
 	@Column(name="creationdate", nullable=false, insertable=true, updatable=false)
 	private Date creationDate;
+	
+	@Column(name="f_refresname", nullable=true, insertable=true, updatable=false)
+	private String refResName;
+	@Column(name="f_refresid", nullable=true, insertable=true, updatable=false)
+	private Long refResId;
 
 	@Override
 	public Long getKey() {
@@ -108,6 +114,33 @@ public class ForumImpl implements Forum, CreateInfo, Persistable{
 		return id;
 	}
 	
+	@Override
+	public void setReference(OLATResourceable ores) {
+		if (ores != null) {
+			this.refResName = ores.getResourceableTypeName();
+			this.refResId = ores.getResourceableId();
+		} else {
+			this.refResName = null;
+			this.refResId = null;
+		}
+	}
+	
+	@Override
+	public OLATResourceable getReference() {
+		return new OLATResourceable() {
+			
+			@Override
+			public String getResourceableTypeName() {
+				return refResName;
+			}
+			
+			@Override
+			public Long getResourceableId() {
+				return refResId;
+			}
+		};
+	}
+	
 	@Override
 	public int hashCode() {
 		return key == null ? 835245 : key.hashCode();
diff --git a/src/main/java/org/olat/repository/RepositoryEntryManagedFlag.java b/src/main/java/org/olat/repository/RepositoryEntryManagedFlag.java
index 591545ab9fe..b6011ea310b 100644
--- a/src/main/java/org/olat/repository/RepositoryEntryManagedFlag.java
+++ b/src/main/java/org/olat/repository/RepositoryEntryManagedFlag.java
@@ -50,6 +50,7 @@ public enum RepositoryEntryManagedFlag {
       participantList(settings, all),
       participantInfo(settings, all),
       email(settings, all),
+      forum(settings, all),
       chat(settings,all),
       layout(settings,all),
       resourcefolder(settings,all),
diff --git a/src/main/resources/database/mysql/alter_14_2_x_to_15_0_0.sql b/src/main/resources/database/mysql/alter_14_2_x_to_15_0_0.sql
index 3171b6b3f85..3daaf6c051d 100644
--- a/src/main/resources/database/mysql/alter_14_2_x_to_15_0_0.sql
+++ b/src/main/resources/database/mysql/alter_14_2_x_to_15_0_0.sql
@@ -3,3 +3,8 @@ alter table o_as_entry add a_first_visit datetime;
 alter table o_as_entry add a_last_visit datetime;
 alter table o_as_entry add a_num_visits int8;
 alter table o_as_entry add a_date_done datetime;
+
+-- forum
+alter table o_forum add f_refresname varchar(50);
+alter table o_forum add f_refresid bigint;
+create index idx_forum_ref_idx on o_forum (f_refresid, f_refresname);
diff --git a/src/main/resources/database/mysql/setupDatabase.sql b/src/main/resources/database/mysql/setupDatabase.sql
index 5f619e2b8cd..daeb2b0b2ed 100644
--- a/src/main/resources/database/mysql/setupDatabase.sql
+++ b/src/main/resources/database/mysql/setupDatabase.sql
@@ -4,6 +4,8 @@ create table if not exists o_forum (
    forum_id bigint not null,
    version mediumint unsigned not null,
    creationdate datetime,
+   f_refresname varchar(50),
+   f_refresid bigint,
    primary key (forum_id)
 );
 create table o_forum_pseudonym (
@@ -3547,6 +3549,7 @@ create index mark_subpath_idx on o_mark(ressubpath(255));
 create index mark_businesspath_idx on o_mark(businesspath(255));
 
 -- forum
+create index idx_forum_ref_idx on o_forum (f_refresid, f_refresname);
 alter table o_message add constraint FKF26C8375236F20E foreign key (creator_id) references o_bs_identity (id);
 alter table o_message add constraint FKF26C837A3FBEB83 foreign key (modifier_id) references o_bs_identity (id);
 alter table o_message add constraint FKF26C8377B66B0D0 foreign key (parent_id) references o_message (message_id);
diff --git a/src/main/resources/database/oracle/alter_14_2_x_to_15_0_0.sql b/src/main/resources/database/oracle/alter_14_2_x_to_15_0_0.sql
index cf8eba16f17..a9e5d98d13f 100644
--- a/src/main/resources/database/oracle/alter_14_2_x_to_15_0_0.sql
+++ b/src/main/resources/database/oracle/alter_14_2_x_to_15_0_0.sql
@@ -3,3 +3,9 @@ alter table o_as_entry add a_first_visit date;
 alter table o_as_entry add a_last_visit date;
 alter table o_as_entry add a_num_visits number(20);
 alter table o_as_entry add a_date_done date;
+
+-- Forum
+alter table o_forum add f_refresname varchar(50);
+alter table o_forum add f_refresid number(20);
+create index idx_forum_ref_idx on o_forum (f_refresid, f_refresname);
+
diff --git a/src/main/resources/database/oracle/setupDatabase.sql b/src/main/resources/database/oracle/setupDatabase.sql
index 9da5127079a..eb0df25fc81 100644
--- a/src/main/resources/database/oracle/setupDatabase.sql
+++ b/src/main/resources/database/oracle/setupDatabase.sql
@@ -5,6 +5,8 @@ CREATE TABLE o_forum (
   forum_id number(20) NOT NULL,
   version number(20) NOT NULL,
   creationdate date,
+  f_refresname varchar(50),
+  f_refresid number(20),
   PRIMARY KEY (forum_id)
 );
 CREATE TABLE o_forum_pseudonym (
@@ -3563,6 +3565,7 @@ create index mark_subpath_idx on o_mark(substr(ressubpath,0,255));
 create index mark_businesspath_idx on o_mark(substr(businesspath,0,255));
 
 -- forum
+create index idx_forum_ref_idx on o_forum (f_refresid, f_refresname);
 alter table o_message add constraint FKF26C8375236F20E foreign key (creator_id) references o_bs_identity (id);
 create index FKF26C8375236F20E on o_message (creator_id);
 alter table o_message add constraint FKF26C837A3FBEB83 foreign key (modifier_id) references o_bs_identity (id);
diff --git a/src/main/resources/database/postgresql/alter_14_2_x_to_15_0_0.sql b/src/main/resources/database/postgresql/alter_14_2_x_to_15_0_0.sql
index 806c9d9c5c0..d83f14debe3 100644
--- a/src/main/resources/database/postgresql/alter_14_2_x_to_15_0_0.sql
+++ b/src/main/resources/database/postgresql/alter_14_2_x_to_15_0_0.sql
@@ -3,3 +3,8 @@ alter table o_as_entry add a_first_visit timestamp;
 alter table o_as_entry add a_last_visit timestamp;
 alter table o_as_entry add a_num_visits int8;
 alter table o_as_entry add a_date_done timestamp;
+
+-- Forum
+alter table o_forum add f_refresname varchar(50);
+alter table o_forum add f_refresid bigint;
+create index idx_forum_ref_idx on o_forum (f_refresid, f_refresname);
diff --git a/src/main/resources/database/postgresql/setupDatabase.sql b/src/main/resources/database/postgresql/setupDatabase.sql
index 1a40cb4c398..48e3fcb881f 100644
--- a/src/main/resources/database/postgresql/setupDatabase.sql
+++ b/src/main/resources/database/postgresql/setupDatabase.sql
@@ -2,6 +2,8 @@ create table o_forum (
    forum_id int8 not null,
    version int4 not null,
    creationdate timestamp,
+   f_refresname varchar(50),
+   f_refresid bigint,
    primary key (forum_id)
 );
 create table o_forum_pseudonym (
@@ -3456,6 +3458,7 @@ create index mark_subpath_idx on o_mark(ressubpath);
 create index mark_businesspath_idx on o_mark(businesspath);
 
 -- forum
+create index idx_forum_ref_idx on o_forum (f_refresid, f_refresname);
 alter table o_message add constraint FKF26C8375236F20E foreign key (creator_id) references o_bs_identity;
 create index idx_message_creator_idx on o_message (creator_id);
 alter table o_message add constraint FKF26C837A3FBEB83 foreign key (modifier_id) references o_bs_identity;
diff --git a/src/test/java/org/olat/modules/fo/ForumManagerTest.java b/src/test/java/org/olat/modules/fo/ForumManagerTest.java
index 121ef64eae2..2bd6074c560 100644
--- a/src/test/java/org/olat/modules/fo/ForumManagerTest.java
+++ b/src/test/java/org/olat/modules/fo/ForumManagerTest.java
@@ -45,6 +45,7 @@ import org.olat.modules.fo.model.ForumUserStatistics;
 import org.olat.modules.fo.model.MessageImpl;
 import org.olat.modules.fo.model.PseudonymStatistics;
 import org.olat.modules.fo.ui.MessagePeekview;
+import org.olat.repository.RepositoryEntry;
 import org.olat.test.JunitTestHelper;
 import org.olat.test.OlatTestCase;
 import org.olat.user.UserManager;
@@ -67,6 +68,27 @@ public class ForumManagerTest extends OlatTestCase {
 	@Autowired
 	public BaseSecurity securityManager;
 	
+	@Test
+	public void addAForum() {
+		Forum fo = forumManager.addAForum();
+		dbInstance.commitAndCloseSession();
+		
+		fo = forumManager.loadForum(fo.getKey());
+		
+		Assert.assertNotNull(fo);
+	}
+	
+	@Test
+	public void addAForumForOlatResourceable() {
+		RepositoryEntry repositoryEntry = JunitTestHelper.createAndPersistRepositoryEntry();
+		Forum fo = forumManager.addAForum(repositoryEntry);
+		dbInstance.commitAndCloseSession();
+		
+		fo = forumManager.loadForum(repositoryEntry);
+		
+		Assert.assertNotNull(fo);
+	}
+	
 	@Test
 	public void getThread() {
 		Identity id = JunitTestHelper.createAndPersistIdentityAsRndUser("fo-4");
-- 
GitLab