diff --git a/cms/envs/common.py b/cms/envs/common.py
index aacfdeafa9..75ff2f823e 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -494,6 +494,7 @@ IDA_LOGOUT_URI_LIST = []
############################# MICROFRONTENDS ###################################
COURSE_AUTHORING_MICROFRONTEND_URL = None
+DISCUSSIONS_MICROFRONTEND_URL = None
LIBRARY_AUTHORING_MICROFRONTEND_URL = None
############################# SOCIAL MEDIA SHARING #############################
diff --git a/cms/envs/devstack.py b/cms/envs/devstack.py
index a7c0827782..469bc20454 100644
--- a/cms/envs/devstack.py
+++ b/cms/envs/devstack.py
@@ -162,6 +162,9 @@ LIBRARY_AUTHORING_MICROFRONTEND_URL = 'http://localhost:3001'
################### FRONTEND APPLICATION COURSE AUTHORING ###################
COURSE_AUTHORING_MICROFRONTEND_URL = 'http://localhost:2001'
+################### FRONTEND APPLICATION DISCUSSIONS ###################
+DISCUSSIONS_MICROFRONTEND_URL = 'http://localhost:2002'
+
################################# DJANGO-REQUIRE ###############################
# Whether to run django-require in debug mode.
diff --git a/lms/djangoapps/courseware/tests/test_discussion_xblock.py b/lms/djangoapps/courseware/tests/test_discussion_xblock.py
index e48973570a..f7482187d1 100644
--- a/lms/djangoapps/courseware/tests/test_discussion_xblock.py
+++ b/lms/djangoapps/courseware/tests/test_discussion_xblock.py
@@ -12,15 +12,20 @@ import uuid
from unittest import mock
import ddt
+from django.test import override_settings
from django.urls import reverse
+from edx_toggles.toggles.testutils import override_waffle_flag
+from opaque_keys.edx.keys import CourseKey
from web_fragments.fragment import Fragment
from xblock.field_data import DictFieldData
from lms.djangoapps.course_api.blocks.tests.helpers import deserialize_usage_key
from lms.djangoapps.courseware.module_render import get_module_for_descriptor_internal
from lms.djangoapps.courseware.tests.helpers import XModuleRenderingTestBase
+from lms.djangoapps.discussion.toggles import ENABLE_DISCUSSIONS_MFE
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
from xblock_discussion import DiscussionXBlock, loader
+
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import ItemFactory, ToyCourseFactory
@@ -40,7 +45,7 @@ class TestDiscussionXBlock(XModuleRenderingTestBase):
"""
super().setUp()
self.patchers = []
- self.course_id = "test_course"
+ self.course_id = CourseKey.from_string("course-v1:test+test+test_course")
self.runtime = self.new_module_runtime()
self.runtime.modulestore = mock.Mock()
@@ -220,7 +225,7 @@ class TestTemplates(TestDiscussionXBlock):
) as has_perm:
actual_permission = self.block.has_permission("test_permission")
assert actual_permission == permission_canary
- has_perm.assert_called_once_with(self.django_user_canary, 'test_permission', 'test_course')
+ has_perm.assert_called_once_with(self.django_user_canary, 'test_permission', self.course_id)
def test_studio_view(self):
"""Test for studio view."""
@@ -304,6 +309,35 @@ class TestXBlockInCourse(SharedModuleStoreTestCase):
assert 'data-user-create-comment="false"' in html
assert 'data-user-create-subcomment="false"' in html
+ @override_settings(DISCUSSIONS_MICROFRONTEND_URL="http://test.url")
+ @override_waffle_flag(ENABLE_DISCUSSIONS_MFE, True)
+ def test_embed_mfe_in_course(self):
+ """
+ Test that the xblock embeds the MFE UI when the flag is enabled
+ """
+ discussion_xblock = get_module_for_descriptor_internal(
+ user=self.user,
+ descriptor=self.discussion,
+ student_data=mock.Mock(name='student_data'),
+ course_id=self.course.id,
+ track_function=mock.Mock(name='track_function'),
+ xqueue_callback_url_prefix=mock.Mock(name='xqueue_callback_url_prefix'),
+ request_token='request_token',
+ )
+
+ fragment = discussion_xblock.render('student_view')
+ html = fragment.content
+ self.assertInHTML(
+ """
+
+ """,
+ html,
+ )
+
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_discussion_render_successfully_with_orphan_parent(self, default_store):
"""
@@ -400,11 +434,12 @@ class TestXBlockQueryLoad(SharedModuleStoreTestCase):
discussion_target='Target Discussion',
))
- # 3 queries are required to do first discussion xblock render:
+ # 4 queries are required to do first discussion xblock render:
+ # * waffle_utils_wafflecourseoverridemodel
+ # * waffle_flag
# * django_comment_client_role
- # * django_comment_client_permission
# * lms_xblock_xblockasidesconfig
- num_queries = 2
+ num_queries = 4
for discussion in discussions:
discussion_xblock = get_module_for_descriptor_internal(
user=user,
@@ -418,8 +453,9 @@ class TestXBlockQueryLoad(SharedModuleStoreTestCase):
with self.assertNumQueries(num_queries):
fragment = discussion_xblock.render('student_view')
- # Permissions are cached, so no queries required for subsequent renders
- num_queries = 0
+ # Permissions are cached, so only 1 query required for subsequent renders
+ # to check the waffle flag
+ num_queries = 1
html = fragment.content
assert 'data-user-create-comment="false"' in html
diff --git a/lms/djangoapps/discussion/django_comment_client/tests/test_utils.py b/lms/djangoapps/discussion/django_comment_client/tests/test_utils.py
index 923fbad871..c11ed02842 100644
--- a/lms/djangoapps/discussion/django_comment_client/tests/test_utils.py
+++ b/lms/djangoapps/discussion/django_comment_client/tests/test_utils.py
@@ -9,9 +9,10 @@ from unittest.mock import Mock, patch
import ddt
import pytest
-from django.test import RequestFactory, TestCase
+from django.test import RequestFactory, TestCase, override_settings
from django.urls import reverse
from edx_django_utils.cache import RequestCache
+from edx_toggles.toggles.testutils import override_waffle_flag
from opaque_keys.edx.keys import CourseKey
from pytz import UTC
@@ -26,6 +27,7 @@ from lms.djangoapps.discussion.django_comment_client.constants import TYPE_ENTRY
from lms.djangoapps.discussion.django_comment_client.tests.factories import RoleFactory
from lms.djangoapps.discussion.django_comment_client.tests.unicode import UnicodeTestMixin
from lms.djangoapps.discussion.django_comment_client.tests.utils import config_course_discussions, topic_name_to_id
+from lms.djangoapps.discussion.toggles import ENABLE_DISCUSSIONS_MFE
from lms.djangoapps.teams.tests.factories import CourseTeamFactory
from openedx.core.djangoapps.course_groups import cohorts
from openedx.core.djangoapps.course_groups.cohorts import set_course_cohorted
@@ -53,6 +55,7 @@ from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_MODULESTORE, ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, ToyCourseFactory
+from xmodule.tabs import CourseTabList
class DictionaryTestCase(TestCase):
@@ -1198,6 +1201,7 @@ class JsonResponseTestCase(TestCase, UnicodeTestMixin):
assert reparsed == text
+@ddt.ddt
class DiscussionTabTestCase(ModuleStoreTestCase):
""" Test visibility of the discussion tab. """
@@ -1230,6 +1234,20 @@ class DiscussionTabTestCase(ModuleStoreTestCase):
with self.settings(FEATURES={'CUSTOM_COURSES_EDX': True}):
assert not self.discussion_tab_present(self.enrolled_user)
+ @override_settings(DISCUSSIONS_MICROFRONTEND_URL="http://test.url")
+ @ddt.data(
+ (True, 'http://test.url/discussions/{}/'),
+ (False, '/courses/{}/discussion/forum/'),
+ )
+ @ddt.unpack
+ def test_tab_with_mfe_flag(self, mfe_enabled, tab_link):
+ """
+ Tests that the correct link is used for the MFE tab
+ """
+ discussion_tab = CourseTabList.get_tab_by_type(self.course.tabs, 'discussion')
+ with override_waffle_flag(ENABLE_DISCUSSIONS_MFE, mfe_enabled):
+ assert discussion_tab.link_func(self.course, reverse) == tab_link.format(self.course.id)
+
class IsCommentableDividedTestCase(ModuleStoreTestCase):
"""
diff --git a/lms/djangoapps/discussion/plugins.py b/lms/djangoapps/discussion/plugins.py
index b9774d4cd8..b35461c3e5 100644
--- a/lms/djangoapps/discussion/plugins.py
+++ b/lms/djangoapps/discussion/plugins.py
@@ -8,6 +8,7 @@ from django.utils.translation import gettext_noop
import lms.djangoapps.discussion.django_comment_client.utils as utils
from lms.djangoapps.courseware.tabs import EnrolledTab
+from lms.djangoapps.discussion.toggles import ENABLE_DISCUSSIONS_MFE
from openedx.features.lti_course_tab.tab import DiscussionLtiCourseTab
from xmodule.tabs import TabFragmentViewMixin
@@ -27,6 +28,19 @@ class DiscussionTab(TabFragmentViewMixin, EnrolledTab):
body_class = 'discussion'
online_help_token = 'discussions'
+ @property
+ def link_func(self):
+ """ Returns a function that returns the course tab's URL. """
+ _link_func = super().link_func
+
+ def link_func(course, reverse_func):
+ """ Returns a function that returns the course tab's URL. """
+ if ENABLE_DISCUSSIONS_MFE.is_enabled(course.id) and settings.DISCUSSIONS_MICROFRONTEND_URL:
+ return f"{settings.DISCUSSIONS_MICROFRONTEND_URL}/discussions/{course.id}/"
+ return _link_func(course, reverse_func)
+
+ return link_func
+
@classmethod
def is_enabled(cls, course, user=None):
if not super().is_enabled(course, user):
diff --git a/lms/djangoapps/discussion/toggles.py b/lms/djangoapps/discussion/toggles.py
new file mode 100644
index 0000000000..31a7f5ac16
--- /dev/null
+++ b/lms/djangoapps/discussion/toggles.py
@@ -0,0 +1,15 @@
+"""
+Discussions feature toggles
+"""
+from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
+
+WAFFLE_FLAG_NAMESPACE = "discussions"
+
+# .. toggle_name: discussions.enable_discussions_mfe
+# .. toggle_implementation: CourseWaffleFlag
+# .. toggle_default: False
+# .. toggle_description: Waffle flag to use the new MFE experience for discussions in the course tab and in-context
+# .. toggle_use_cases: temporary, open_edx
+# .. toggle_creation_date: 2021-11-05
+# .. toggle_target_removal_date: 2022-03-05
+ENABLE_DISCUSSIONS_MFE = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'enable_discussions_mfe', __name__)
diff --git a/lms/djangoapps/discussion/views.py b/lms/djangoapps/discussion/views.py
index 6cfdf37101..e232476031 100644
--- a/lms/djangoapps/discussion/views.py
+++ b/lms/djangoapps/discussion/views.py
@@ -45,6 +45,7 @@ from lms.djangoapps.discussion.django_comment_client.utils import (
strip_none,
)
from lms.djangoapps.discussion.exceptions import TeamDiscussionHiddenFromUserException
+from lms.djangoapps.discussion.toggles import ENABLE_DISCUSSIONS_MFE
from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context
from lms.djangoapps.teams import api as team_api
from openedx.core.djangoapps.discussions.utils import (
@@ -56,6 +57,7 @@ from openedx.core.djangoapps.discussions.utils import (
from openedx.core.djangoapps.django_comment_common.models import CourseDiscussionSettings
from openedx.core.djangoapps.django_comment_common.utils import ThreadContext
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
+from openedx.core.djangolib.markup import HTML
from openedx.features.course_duration_limits.access import generate_course_expired_fragment
from xmodule.modulestore.django import modulestore
@@ -717,8 +719,24 @@ class DiscussionBoardFragmentView(EdxFragmentView):
Returns:
Fragment: The fragment representing the discussion board
"""
+ course_key = CourseKey.from_string(course_id)
+ if ENABLE_DISCUSSIONS_MFE.is_enabled(course_key) and settings.DISCUSSIONS_MICROFRONTEND_URL:
+ fragment = Fragment(
+ HTML(
+ ""
+ ).format(src=f"{settings.DISCUSSIONS_MICROFRONTEND_URL}/discussions/{course_id}/")
+ )
+ fragment.add_css(
+ """
+ #discussions-mfe-tab-embed {
+ width: 100%;
+ min-height: 800px;
+ border: none;
+ }
+ """
+ )
+ return fragment
try:
- course_key = CourseKey.from_string(course_id)
base_context = _create_base_discussion_view_context(request, course_key)
# Note:
# After the thread is rendered in this fragment, an AJAX
diff --git a/lms/envs/common.py b/lms/envs/common.py
index fa902bf7b2..57f2026354 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -4683,6 +4683,11 @@ PROGRAM_CONSOLE_MICROFRONTEND_URL = None
# .. setting_description: Base URL of the micro-frontend-based courseware page.
# .. setting_warning: Also set site's courseware.courseware_mfe waffle flag.
LEARNING_MICROFRONTEND_URL = None
+# .. setting_name: DISCUSSIONS_MICROFRONTEND_URL
+# .. setting_default: None
+# .. setting_description: Base URL of the micro-frontend-based dicussions page.
+# .. setting_warning: Also set site's courseware.discussions_mfe waffle flag.
+DISCUSSIONS_MICROFRONTEND_URL = None
############### Settings for the ace_common plugin #################
ACE_ENABLED_CHANNELS = ['django_email']
diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py
index 608928f55b..fe422a1cef 100644
--- a/lms/envs/devstack.py
+++ b/lms/envs/devstack.py
@@ -338,6 +338,9 @@ ACCOUNT_MICROFRONTEND_URL = 'http://localhost:1997'
AUTHN_MICROFRONTEND_URL = 'http://localhost:1999'
AUTHN_MICROFRONTEND_DOMAIN = 'localhost:1999'
+################### FRONTEND APPLICATION DISCUSSIONS ###################
+DISCUSSIONS_MICROFRONTEND_URL = 'http://localhost:2002'
+
############## Docker based devstack settings #######################
FEATURES.update({
diff --git a/openedx/core/lib/xblock_builtin/xblock_discussion/xblock_discussion/__init__.py b/openedx/core/lib/xblock_builtin/xblock_discussion/xblock_discussion/__init__.py
index 079b1765cb..264b88725d 100644
--- a/openedx/core/lib/xblock_builtin/xblock_discussion/xblock_discussion/__init__.py
+++ b/openedx/core/lib/xblock_builtin/xblock_discussion/xblock_discussion/__init__.py
@@ -4,16 +4,19 @@ Discussion XBlock
import logging
import urllib
+
+from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage
from django.urls import reverse
from django.utils.translation import get_language_bidi
+from web_fragments.fragment import Fragment
from xblock.completable import XBlockCompletionMode
from xblock.core import XBlock
from xblock.fields import Scope, String, UNIQUE_ID
-from web_fragments.fragment import Fragment
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin
+from lms.djangoapps.discussion.toggles import ENABLE_DISCUSSIONS_MFE
from openedx.core.djangolib.markup import HTML, Text
from openedx.core.lib.xblock_builtin import get_css_dependencies, get_js_dependencies
from xmodule.xml_module import XmlParserMixin
@@ -166,6 +169,21 @@ class DiscussionXBlock(XBlock, StudioEditableXBlockMixin, XmlParserMixin): # li
Renders student view for LMS.
"""
fragment = Fragment()
+ if ENABLE_DISCUSSIONS_MFE.is_enabled(self.course_key) and settings.DISCUSSIONS_MICROFRONTEND_URL:
+ url = f"{settings.DISCUSSIONS_MICROFRONTEND_URL}/discussions/{self.course_key}/topics/{self.discussion_id}"
+ fragment.add_content(HTML(
+ ""
+ ).format(src=url, title=_("Discussions")))
+ fragment.add_css(
+ """
+ #discussions-mfe-tab-embed {
+ width: 100%;
+ height: 800px;
+ border: none;
+ }
+ """
+ )
+ return fragment
self.add_resource_urls(fragment)