Merge pull request #7482 from open-craft/fix_duplicate_rcb
Fix duplication of Randomized Content Blocks
This commit is contained in:
@@ -3,6 +3,7 @@ Content library unit tests that require the CMS runtime.
|
||||
"""
|
||||
from contentstore.tests.utils import AjaxEnabledTestClient, parse_json
|
||||
from contentstore.utils import reverse_url, reverse_usage_url, reverse_library_url
|
||||
from contentstore.views.item import _duplicate_item
|
||||
from contentstore.views.preview import _load_preview_module
|
||||
from contentstore.views.tests.test_library import LIBRARY_REST_URL
|
||||
import ddt
|
||||
@@ -726,6 +727,7 @@ class TestLibraryAccess(SignalDisconnectTestMixin, LibraryTestCase):
|
||||
self.assertEqual(len(lc_block.children), 1 if expected_result else 0)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestOverrides(LibraryTestCase):
|
||||
"""
|
||||
Test that overriding block Scope.settings fields from a library in a specific course works
|
||||
@@ -745,6 +747,9 @@ class TestOverrides(LibraryTestCase):
|
||||
publish_item=False,
|
||||
)
|
||||
|
||||
# Refresh library now that we've added something.
|
||||
self.library = modulestore().get_library(self.lib_key)
|
||||
|
||||
# Also create a course:
|
||||
with modulestore().default_store(ModuleStoreEnum.Type.split):
|
||||
self.course = CourseFactory.create()
|
||||
@@ -822,7 +827,8 @@ class TestOverrides(LibraryTestCase):
|
||||
self.assertEqual(self.problem.definition_locator.definition_id, definition_id)
|
||||
self.assertEqual(self.problem_in_course.definition_locator.definition_id, definition_id)
|
||||
|
||||
def test_persistent_overrides(self):
|
||||
@ddt.data(False, True)
|
||||
def test_persistent_overrides(self, duplicate):
|
||||
"""
|
||||
Test that when we override Scope.settings values in a course,
|
||||
the override values persist even when the block is refreshed
|
||||
@@ -834,7 +840,14 @@ class TestOverrides(LibraryTestCase):
|
||||
self.problem_in_course.weight = new_weight
|
||||
|
||||
modulestore().update_item(self.problem_in_course, self.user.id)
|
||||
self.problem_in_course = modulestore().get_item(self.problem_in_course.location)
|
||||
if duplicate:
|
||||
# Check that this also works when the RCB is duplicated.
|
||||
self.lc_block = modulestore().get_item(
|
||||
_duplicate_item(self.course.location, self.lc_block.location, self.user)
|
||||
)
|
||||
self.problem_in_course = modulestore().get_item(self.lc_block.children[0])
|
||||
else:
|
||||
self.problem_in_course = modulestore().get_item(self.problem_in_course.location)
|
||||
self.assertEqual(self.problem_in_course.display_name, new_display_name)
|
||||
self.assertEqual(self.problem_in_course.weight, new_weight)
|
||||
|
||||
@@ -852,6 +865,52 @@ class TestOverrides(LibraryTestCase):
|
||||
self.assertEqual(self.problem_in_course.weight, new_weight)
|
||||
self.assertEqual(self.problem_in_course.data, new_data_value)
|
||||
|
||||
def test_duplicated_version(self):
|
||||
"""
|
||||
Test that if a library is updated, and the content block is duplicated,
|
||||
the new block will use the old library version and not the new one.
|
||||
"""
|
||||
store = modulestore()
|
||||
self.assertEqual(len(self.library.children), 1)
|
||||
self.assertEqual(len(self.lc_block.children), 1)
|
||||
|
||||
# Edit the only problem in the library:
|
||||
self.problem.display_name = "--changed in library--"
|
||||
store.update_item(self.problem, self.user.id)
|
||||
# Create an additional problem block in the library:
|
||||
ItemFactory.create(
|
||||
category="problem",
|
||||
parent_location=self.library.location,
|
||||
user_id=self.user.id,
|
||||
publish_item=False,
|
||||
)
|
||||
|
||||
# Refresh our reference to the library
|
||||
self.library = store.get_library(self.lib_key)
|
||||
|
||||
# Refresh our reference to the block
|
||||
self.lc_block = store.get_item(self.lc_block.location)
|
||||
self.problem_in_course = store.get_item(self.problem_in_course.location)
|
||||
|
||||
# The library has changed...
|
||||
self.assertEqual(len(self.library.children), 2)
|
||||
|
||||
# But the block hasn't.
|
||||
self.assertEqual(len(self.lc_block.children), 1)
|
||||
self.assertEqual(self.problem_in_course.location, self.lc_block.children[0])
|
||||
self.assertEqual(self.problem_in_course.display_name, self.original_display_name)
|
||||
|
||||
# Duplicate self.lc_block:
|
||||
duplicate = store.get_item(
|
||||
_duplicate_item(self.course.location, self.lc_block.location, self.user)
|
||||
)
|
||||
# The duplicate should have identical children to the original:
|
||||
self.assertEqual(len(duplicate.children), 1)
|
||||
self.assertTrue(self.lc_block.source_library_version)
|
||||
self.assertEqual(self.lc_block.source_library_version, duplicate.source_library_version)
|
||||
problem2_in_course = store.get_item(duplicate.children[0])
|
||||
self.assertEqual(problem2_in_course.display_name, self.original_display_name)
|
||||
|
||||
|
||||
class TestIncompatibleModuleStore(LibraryTestCase):
|
||||
"""
|
||||
|
||||
@@ -593,16 +593,25 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_
|
||||
runtime=source_item.runtime,
|
||||
)
|
||||
|
||||
children_handled = False
|
||||
|
||||
if hasattr(dest_module, 'studio_post_duplicate'):
|
||||
# Allow an XBlock to do anything fancy it may need to when duplicated from another block.
|
||||
# These blocks may handle their own children or parenting if needed. Let them return booleans to
|
||||
# let us know if we need to handle these or not.
|
||||
children_handled = dest_module.studio_post_duplicate(store, source_item)
|
||||
|
||||
# Children are not automatically copied over (and not all xblocks have a 'children' attribute).
|
||||
# Because DAGs are not fully supported, we need to actually duplicate each child as well.
|
||||
if source_item.has_children:
|
||||
dest_module.children = []
|
||||
if source_item.has_children and not children_handled:
|
||||
dest_module.children = dest_module.children or []
|
||||
for child in source_item.children:
|
||||
dupe = _duplicate_item(dest_module.location, child, user=user)
|
||||
if dupe not in dest_module.children: # _duplicate_item may add the child for us.
|
||||
dest_module.children.append(dupe)
|
||||
store.update_item(dest_module, user.id)
|
||||
|
||||
# pylint: disable=protected-access
|
||||
if 'detached' not in source_item.runtime.load_block_type(category)._class_tags:
|
||||
parent = store.get_item(parent_usage_key)
|
||||
# If source was already a child of the parent, add duplicate immediately afterward.
|
||||
|
||||
@@ -240,7 +240,6 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
|
||||
# Only add the Studio wrapper when on the container page. The "Pages" page will remain as is for now.
|
||||
if not context.get('is_pages_view', None) and view in PREVIEW_VIEWS:
|
||||
root_xblock = context.get('root_xblock')
|
||||
can_edit_visibility = not isinstance(xblock.location, LibraryUsageLocator)
|
||||
is_root = root_xblock and xblock.location == root_xblock.location
|
||||
is_reorderable = _is_xblock_reorderable(xblock, context)
|
||||
template_context = {
|
||||
@@ -251,7 +250,8 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
|
||||
'is_root': is_root,
|
||||
'is_reorderable': is_reorderable,
|
||||
'can_edit': context.get('can_edit', True),
|
||||
'can_edit_visibility': can_edit_visibility,
|
||||
'can_edit_visibility': context.get('can_edit_visibility', True),
|
||||
'can_add': context.get('can_add', True),
|
||||
}
|
||||
html = render_to_string('studio_xblock_wrapper.html', template_context)
|
||||
frag = wrap_fragment(frag, html)
|
||||
|
||||
@@ -80,19 +80,24 @@ messages = json.dumps(xblock.validate().to_json())
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
<li class="action-item action-duplicate">
|
||||
<a href="#" data-tooltip="${_("Duplicate")}" class="duplicate-button action-button">
|
||||
<i class="icon fa fa-copy"></i>
|
||||
<span class="sr">${_("Duplicate")}</span>
|
||||
% if can_add:
|
||||
<li class="action-item action-duplicate">
|
||||
<a href="#" data-tooltip="${_("Duplicate")}" class="duplicate-button action-button">
|
||||
<i class="icon fa fa-copy"></i>
|
||||
<span class="sr">${_("Duplicate")}</span>
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
% endif
|
||||
% if can_add:
|
||||
<!-- If we can add, we can delete. -->
|
||||
<li class="action-item action-delete">
|
||||
<a href="#" data-tooltip="${_("Delete")}" class="delete-button action-button">
|
||||
<i class="icon fa fa-trash-o"></i>
|
||||
<span class="sr">${_("Delete")}</span>
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
<li class="action-item action-delete">
|
||||
<a href="#" data-tooltip="${_("Delete")}" class="delete-button action-button">
|
||||
<i class="icon fa fa-trash-o"></i>
|
||||
<span class="sr">${_("Delete")}</span>
|
||||
</a>
|
||||
</li>
|
||||
% if is_reorderable:
|
||||
<li class="action-item action-drag">
|
||||
<span data-tooltip="${_('Drag to reorder')}" class="drag-handle action"></span>
|
||||
|
||||
Reference in New Issue
Block a user