diff --git a/cms/djangoapps/contentstore/helpers.py b/cms/djangoapps/contentstore/helpers.py
index 0d66070b1120..2bfdd6574aad 100644
--- a/cms/djangoapps/contentstore/helpers.py
+++ b/cms/djangoapps/contentstore/helpers.py
@@ -17,6 +17,7 @@
from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import contentstore
from xmodule.exceptions import NotFoundError
+from xmodule.library_content_block import LibraryContentBlock
from xmodule.modulestore.django import modulestore
from xmodule.xml_block import XmlMixin
@@ -336,8 +337,14 @@ def _import_xml_node_to_parent(
new_xblock = store.update_item(temp_xblock, user_id, allow_not_found=True)
parent_xblock.children.append(new_xblock.location)
store.update_item(parent_xblock, user_id)
- for child_node in child_nodes:
- _import_xml_node_to_parent(child_node, new_xblock, store, user_id=user_id)
+ if isinstance(new_xblock, LibraryContentBlock):
+ # Special case handling for library content. If we need this for other blocks in the future, it can be made into
+ # an API, and we'd call new_block.studio_post_paste() instead of this code.
+ # In this case, we want to pull the children from the library and let library_tools assign their IDs.
+ new_xblock.tools.update_children(new_xblock, version=new_xblock.source_library_version)
+ else:
+ for child_node in child_nodes:
+ _import_xml_node_to_parent(child_node, new_xblock, store, user_id=user_id)
return new_xblock
diff --git a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py
index a447799c2950..bdf22532fd5f 100644
--- a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py
+++ b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py
@@ -4,11 +4,15 @@
APIs.
"""
import ddt
+from django.test import LiveServerTestCase
from opaque_keys.edx.keys import UsageKey
from rest_framework.test import APIClient
-from xmodule.modulestore.django import contentstore
+from xmodule.modulestore.django import contentstore, modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, upload_file_to_course
-from xmodule.modulestore.tests.factories import BlockFactory, CourseFactory, ToyCourseFactory
+from xmodule.modulestore.tests.factories import BlockFactory, CourseFactory, LibraryFactory, ToyCourseFactory
+
+from cms.djangoapps.contentstore.utils import reverse_usage_url
+from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient
CLIPBOARD_ENDPOINT = "/api/content-staging/v1/clipboard/"
XBLOCK_ENDPOINT = "/xblock/"
@@ -109,7 +113,7 @@ def test_copy_and_paste_component(self, block_args):
"""
Test copying a component (XBlock) from one course into another
"""
- source_course = CourseFactory.create(display_name='Destination Course')
+ source_course = CourseFactory.create(display_name='Source Course')
source_block = BlockFactory.create(parent_location=source_course.location, **block_args)
dest_course = CourseFactory.create(display_name='Destination Course')
@@ -204,3 +208,79 @@ def test_paste_with_assets(self):
source_pic2_hash = contentstore().find(source_course.id.make_asset_key("asset", "picture2.jpg")).content_digest
dest_pic2_hash = contentstore().find(dest_course_key.make_asset_key("asset", "picture2.jpg")).content_digest
assert source_pic2_hash != dest_pic2_hash # Because there was a conflict, this file was unchanged.
+
+
+class ClipboardLibraryContentPasteTestCase(LiveServerTestCase, ModuleStoreTestCase):
+ """
+ Test Clipboard Paste functionality with library content
+ """
+
+ def setUp(self):
+ """
+ Set up a v2 Content Library and a library content block
+ """
+ super().setUp()
+ self.client = AjaxEnabledTestClient()
+ self.client.login(username=self.user.username, password=self.user_password)
+ self.store = modulestore()
+
+ def test_paste_library_content_block_v1(self):
+ """
+ Same as the above test, but uses modulestore (v1) content library
+ """
+ library = LibraryFactory.create()
+ data = {
+ 'parent_locator': str(library.location),
+ 'category': 'html',
+ 'display_name': 'HTML Content',
+ }
+ response = self.client.ajax_post(XBLOCK_ENDPOINT, data)
+ self.assertEqual(response.status_code, 200)
+ course = CourseFactory.create(display_name='Course')
+ orig_lc_block = BlockFactory.create(
+ parent=course,
+ category="library_content",
+ source_library_id=str(library.location.library_key),
+ display_name="LC Block",
+ publish_item=False,
+ )
+ orig_lc_block.refresh_children()
+ orig_child = self.store.get_item(orig_lc_block.children[0])
+ assert orig_child.display_name == "HTML Content"
+ # Copy a library content block that has children:
+ copy_response = self.client.post(CLIPBOARD_ENDPOINT, {
+ "usage_key": str(orig_lc_block.location)
+ }, format="json")
+ assert copy_response.status_code == 200
+
+ # Paste the Library content block:
+ paste_response = self.client.ajax_post(XBLOCK_ENDPOINT, {
+ "parent_locator": str(course.location),
+ "staged_content": "clipboard",
+ })
+ assert paste_response.status_code == 200
+ dest_lc_block_key = UsageKey.from_string(paste_response.json()["locator"])
+
+ # Get the ID of the new child:
+ dest_lc_block = self.store.get_item(dest_lc_block_key)
+ dest_child = self.store.get_item(dest_lc_block.children[0])
+ assert dest_child.display_name == "HTML Content"
+
+ # Importantly, the ID of the child must not changed when the library content is synced.
+ # Otherwise, user state saved against this child will be lost when it syncs.
+ dest_lc_block.refresh_children()
+ updated_dest_child = self.store.get_item(dest_lc_block.children[0])
+ assert dest_child.location == updated_dest_child.location
+
+ def _sync_lc_block_from_library(self, attr_name):
+ """
+ Helper method to "sync" a Library Content Block by [re-]fetching its
+ children from the library.
+ """
+ usage_key = getattr(self, attr_name).location
+ # It's easiest to do this via the REST API:
+ handler_url = reverse_usage_url('preview_handler', usage_key, kwargs={'handler': 'upgrade_and_sync'})
+ response = self.client.post(handler_url)
+ assert response.status_code == 200
+ # Now reload the block and make sure the child is in place
+ setattr(self, attr_name, self.store.get_item(usage_key)) # we must reload after upgrade_and_sync
diff --git a/common/test/data/course_after_rename/about/overview.html b/common/test/data/course_after_rename/about/overview.html
index 0f9a54de9a4a..b20cf86c6d2f 100644
--- a/common/test/data/course_after_rename/about/overview.html
+++ b/common/test/data/course_after_rename/about/overview.html
@@ -14,7 +14,7 @@
Requirements
Course Staff
-
+
Staff Member #1
@@ -23,7 +23,7 @@ Staff Member #1
-
+
Staff Member #2
diff --git a/common/test/data/course_before_rename/about/overview.html b/common/test/data/course_before_rename/about/overview.html
index 0f9a54de9a4a..b20cf86c6d2f 100644
--- a/common/test/data/course_before_rename/about/overview.html
+++ b/common/test/data/course_before_rename/about/overview.html
@@ -14,7 +14,7 @@ Requirements
Course Staff
-
+
Staff Member #1
@@ -23,7 +23,7 @@ Staff Member #1
-
+
Staff Member #2
diff --git a/common/test/data/course_info_updates/about/overview.html b/common/test/data/course_info_updates/about/overview.html
index 6b4fe1a8636d..632157bfa99c 100644
--- a/common/test/data/course_info_updates/about/overview.html
+++ b/common/test/data/course_info_updates/about/overview.html
@@ -14,7 +14,7 @@ Prerequisites
Course Staff
-
+
Staff Member #1
@@ -23,7 +23,7 @@ Staff Member #1
-
+
Staff Member #2
diff --git a/common/test/data/manual-testing-complete/about/overview.html b/common/test/data/manual-testing-complete/about/overview.html
index 6b4fe1a8636d..632157bfa99c 100644
--- a/common/test/data/manual-testing-complete/about/overview.html
+++ b/common/test/data/manual-testing-complete/about/overview.html
@@ -14,7 +14,7 @@ Prerequisites
Course Staff
-
+
Staff Member #1
@@ -23,7 +23,7 @@ Staff Member #1
-
+
Staff Member #2
diff --git a/common/test/data/scoreable/about/overview.html b/common/test/data/scoreable/about/overview.html
index f829d3c8c177..d554b6a01843 100644
--- a/common/test/data/scoreable/about/overview.html
+++ b/common/test/data/scoreable/about/overview.html
@@ -14,7 +14,7 @@ Requirements
Course Staff
-
+
Staff Member #1
@@ -23,7 +23,7 @@ Staff Member #1
-
+
Staff Member #2
diff --git a/openedx/core/djangoapps/content_libraries/api.py b/openedx/core/djangoapps/content_libraries/api.py
index 737673b32e3f..d020a7635b97 100644
--- a/openedx/core/djangoapps/content_libraries/api.py
+++ b/openedx/core/djangoapps/content_libraries/api.py
@@ -91,7 +91,15 @@
from xblock.exceptions import XBlockNotFoundError
from edx_rest_api_client.client import OAuthAPIClient
from openedx.core.djangoapps.content_libraries import permissions
-from openedx.core.djangoapps.content_libraries.constants import DRAFT_NAME, COMPLEX
+# pylint: disable=unused-import
+from openedx.core.djangoapps.content_libraries.constants import (
+ ALL_RIGHTS_RESERVED,
+ CC_4_BY,
+ COMPLEX,
+ DRAFT_NAME,
+ PROBLEM,
+ VIDEO,
+)
from openedx.core.djangoapps.content_libraries.library_bundle import LibraryBundle
from openedx.core.djangoapps.content_libraries.libraries_index import ContentLibraryIndexer, LibraryBlockIndexer
from openedx.core.djangoapps.content_libraries.models import (
@@ -425,6 +433,8 @@ def create_library(
allow_public_read: Allow anyone to view blocks (including source) in Studio?
+ library_type: Deprecated parameter, not really used. Set to COMPLEX.
+
Returns a ContentLibraryMetadata instance.
"""
assert isinstance(collection_uuid, UUID)
diff --git a/openedx/core/djangoapps/theming/template_loaders.py b/openedx/core/djangoapps/theming/template_loaders.py
index 92326711d20a..760f1dd1adb8 100644
--- a/openedx/core/djangoapps/theming/template_loaders.py
+++ b/openedx/core/djangoapps/theming/template_loaders.py
@@ -27,13 +27,17 @@ class ThemeFilesystemLoader(FilesystemLoader):
is_usable = True
_accepts_engine_in_init = True
- def __init__(self, engine, dirs=None):
- if not dirs:
- self.dirs = engine.dirs
+ def get_dirs(self):
+ """
+ Override get_dirs method.
+
+ Make the theme templates a priority, avoiding cashing templates for django ones.
+ """
+ dirs = super().get_dirs()
theme_dirs = self.get_theme_template_sources()
- if isinstance(theme_dirs, list):
- self.dirs = theme_dirs + self.dirs
- super().__init__(engine, self.dirs)
+ if theme_dirs:
+ dirs = theme_dirs + dirs
+ return dirs
@staticmethod
def get_theme_template_sources():
diff --git a/xmodule/templates/about/overview.yaml b/xmodule/templates/about/overview.yaml
index 29ed75b96339..c5ddfcd97ee7 100644
--- a/xmodule/templates/about/overview.yaml
+++ b/xmodule/templates/about/overview.yaml
@@ -19,7 +19,7 @@ data: |
Course Staff
-
+
Staff Member #1
@@ -28,7 +28,7 @@ data: |
-
+
Staff Member #2
diff --git a/xmodule/tests/test_vertical.py b/xmodule/tests/test_vertical.py
index 223cf3889cc7..a3c596d253ac 100644
--- a/xmodule/tests/test_vertical.py
+++ b/xmodule/tests/test_vertical.py
@@ -14,12 +14,10 @@
import pytz
from django.contrib.auth.models import AnonymousUser
from django.test import override_settings
-from edx_toggles.toggles.testutils import override_waffle_switch
from fs.memoryfs import MemoryFS
from openedx_filters import PipelineStep
from openedx_filters.learning.filters import VerticalBlockChildRenderStarted, VerticalBlockRenderCompleted
-from ..vertical_block import XBLOCK_SKILL_TAG_VERIFICATION_SWITCH
from ..x_module import AUTHOR_VIEW, PUBLIC_VIEW, STUDENT_VIEW
from . import prepare_block_runtime
from .helpers import StubUserService
@@ -362,7 +360,6 @@ def test_render_studio_view(self):
},
},
)
- @override_waffle_switch(XBLOCK_SKILL_TAG_VERIFICATION_SWITCH, True)
def test_vertical_block_child_render_started_filter_execution(self):
"""
Test the VerticalBlockChildRenderStarted filter's effects on student view.
@@ -385,7 +382,6 @@ def test_vertical_block_child_render_started_filter_execution(self):
},
},
)
- @override_waffle_switch(XBLOCK_SKILL_TAG_VERIFICATION_SWITCH, True)
def test_vertical_block_child_render_is_skipped_on_filter_exception(self):
"""
Test VerticalBlockChildRenderStarted filter can be used to skip child blocks.
@@ -409,7 +405,6 @@ def test_vertical_block_child_render_is_skipped_on_filter_exception(self):
},
},
)
- @override_waffle_switch(XBLOCK_SKILL_TAG_VERIFICATION_SWITCH, True)
def test_vertical_block_render_completed_filter_execution(self):
"""
Test the VerticalBlockRenderCompleted filter's execution.
@@ -432,7 +427,6 @@ def test_vertical_block_render_completed_filter_execution(self):
},
},
)
- @override_waffle_switch(XBLOCK_SKILL_TAG_VERIFICATION_SWITCH, True)
def test_vertical_block_render_output_is_changed_on_filter_exception(self):
"""
Test VerticalBlockRenderCompleted filter can be used to prevent vertical block from rendering.
diff --git a/xmodule/vertical_block.py b/xmodule/vertical_block.py
index 10ff0a540870..2a10ae44497f 100644
--- a/xmodule/vertical_block.py
+++ b/xmodule/vertical_block.py
@@ -9,7 +9,6 @@
from functools import reduce
import pytz
-from edx_toggles.toggles import WaffleSwitch
from lxml import etree
from openedx_filters.learning.filters import VerticalBlockChildRenderStarted, VerticalBlockRenderCompleted
from web_fragments.fragment import Fragment
@@ -34,17 +33,6 @@
# HACK: This shouldn't be hard-coded to two types
# OBSOLETE: This obsoletes 'type'
CLASS_PRIORITY = ['video', 'problem']
-WAFFLE_NAMESPACE = 'xblocks'
-# .. toggle_name: xblocks.xblock_skill_tag_verification'
-# .. toggle_implementation: WaffleSwitch
-# .. toggle_default: False
-# .. toggle_description: Set to True to get learner verification of the skills associated with the xblock.
-# .. toggle_use_cases: open_edx
-# .. toggle_creation_date: 2023-10-04
-# .. toggle_target_removal_date: None
-XBLOCK_SKILL_TAG_VERIFICATION_SWITCH = WaffleSwitch( # lint-amnesty, pylint: disable=toggle-missing-annotation
- f'{WAFFLE_NAMESPACE}.xblock_skill_tag_verification', __name__
-)
class VerticalFields:
@@ -129,16 +117,15 @@ def _student_or_public_view(self, context, view): # lint-amnesty, pylint: disab
child_block_context['wrap_xblock_data'] = {
'mark-completed-on-view-after-delay': complete_on_view_delay
}
- if XBLOCK_SKILL_TAG_VERIFICATION_SWITCH.is_enabled():
- try:
- # .. filter_implemented_name: VerticalBlockChildRenderStarted
- # .. filter_type: org.openedx.learning.vertical_block_child.render.started.v1
- child, child_block_context = VerticalBlockChildRenderStarted.run_filter(
- block=child, context=child_block_context
- )
- except VerticalBlockChildRenderStarted.PreventChildBlockRender as exc:
- log.info("Skipping %s from vertical block. Reason: %s", child, exc.message)
- continue
+ try:
+ # .. filter_implemented_name: VerticalBlockChildRenderStarted
+ # .. filter_type: org.openedx.learning.vertical_block_child.render.started.v1
+ child, child_block_context = VerticalBlockChildRenderStarted.run_filter(
+ block=child, context=child_block_context
+ )
+ except VerticalBlockChildRenderStarted.PreventChildBlockRender as exc:
+ log.info("Skipping %s from vertical block. Reason: %s", child, exc.message)
+ continue
rendered_child = child.render(view, child_block_context)
fragment.add_fragment_resources(rendered_child)
@@ -180,16 +167,15 @@ def _student_or_public_view(self, context, view): # lint-amnesty, pylint: disab
add_webpack_js_to_fragment(fragment, 'VerticalStudentView')
fragment.initialize_js('VerticalStudentView')
- if XBLOCK_SKILL_TAG_VERIFICATION_SWITCH.is_enabled():
- try:
- # .. filter_implemented_name: VerticalBlockRenderCompleted
- # .. filter_type: org.openedx.learning.vertical_block.render.completed.v1
- _, fragment, context, view = VerticalBlockRenderCompleted.run_filter(
- block=self, fragment=fragment, context=context, view=view
- )
- except VerticalBlockRenderCompleted.PreventVerticalBlockRender as exc:
- log.info("VerticalBlock rendering stopped. Reason: %s", exc.message)
- fragment.content = exc.message
+ try:
+ # .. filter_implemented_name: VerticalBlockRenderCompleted
+ # .. filter_type: org.openedx.learning.vertical_block.render.completed.v1
+ _, fragment, context, view = VerticalBlockRenderCompleted.run_filter(
+ block=self, fragment=fragment, context=context, view=view
+ )
+ except VerticalBlockRenderCompleted.PreventVerticalBlockRender as exc:
+ log.info("VerticalBlock rendering stopped. Reason: %s", exc.message)
+ fragment.content = exc.message
return fragment