feat: Add support for using the discussions MFE UI instead of existing UI [BD-38] [TNL-9228] (#29285)
* feat: Add support for using the discussions MFE UI instead of existing UI Adds a new course waffle flag that when set along with the discussions MFE URL shows the discussions MFE UI instead of the regular UI. * test: add tests * squash!: more consistent url name
This commit is contained in:
@@ -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 #############################
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
"""
|
||||
<iframe
|
||||
id='discussions-mfe-tab-embed'
|
||||
title='Discussions'
|
||||
src='http://test.url/discussions/edX/toy/2012_Fall/topics/test_discussion_xblock_id'
|
||||
/>
|
||||
""",
|
||||
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
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
15
lms/djangoapps/discussion/toggles.py
Normal file
15
lms/djangoapps/discussion/toggles.py
Normal file
@@ -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__)
|
||||
@@ -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(
|
||||
"<iframe id='discussions-mfe-tab-embed' src='{src}'></iframe>"
|
||||
).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
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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(
|
||||
"<iframe id='discussions-mfe-tab-embed' src='{src}' title='{title}'></iframe>"
|
||||
).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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user