feat: provisionally support V2 libraries in LibraryContentBlock (randomized only) (#33263)

Refactors and reworks the LibraryContentBlock so that its
sync-from-library operations are asynchronous and work with
V2 content libraries. This also required us to make
library_content block duplication asynchronous, as that
involves syncing from the source library.

For the sake of clarity, this PR includes two major method renames:

* update_children(...) -> sync_from_library(...)
* refresh_library(...) -> sync_from_library(upgrade_to_latest=True, ...)

an an XBlock HTTP handler rename:

  /refresh_children -> /upgrade_and_sync

There are still a couple issues with import or duplication
of library_content blocks referencing V2 libraries other than
latest. These will be resolved in an upcoming PR.

Part of: https://openedx.atlassian.net/wiki/spaces/COMM/pages/3820617729/Spec+Memo+Content+Library+Authoring+Experience+V2
Follow-up work: https://github.com/openedx/edx-platform/issues/33640

Co-authored-by: Connor Haugh <chaugh@2u.com>
Co-authored-by: Eugene Dyudyunov <evgen.dyudyunov@raccoongang.com>
This commit is contained in:
Kyle McCormick
2023-11-20 10:58:10 -05:00
committed by GitHub
parent c53cf9f1c3
commit e800ae7622
33 changed files with 1597 additions and 573 deletions

View File

@@ -4,16 +4,18 @@ Basic unit tests for LibraryContentBlock
Higher-level tests are in `cms/djangoapps/contentstore/tests/test_libraries.py`.
"""
from unittest.mock import MagicMock, Mock, patch
import ddt
import ddt
from bson.objectid import ObjectId
from fs.memoryfs import MemoryFS
from lxml import etree
from opaque_keys.edx.locator import LibraryLocator, LibraryLocatorV2
from rest_framework import status
from search.search_engine_base import SearchEngine
from web_fragments.fragment import Fragment
from xblock.runtime import Runtime as VanillaRuntime
from rest_framework import status
from openedx.core.djangolib.testing.utils import skip_unless_cms
from xmodule.library_content_block import ANY_CAPA_TYPE_VALUE, LibraryContentBlock
from xmodule.library_tools import LibraryToolsService
from xmodule.modulestore import ModuleStoreEnum
@@ -23,12 +25,14 @@ from xmodule.tests import prepare_block_runtime
from xmodule.validation import StudioValidationMessage
from xmodule.x_module import AUTHOR_VIEW
from xmodule.capa_block import ProblemBlock
from common.djangoapps.student.tests.factories import UserFactory
from .test_course_block import DummySystem as TestImportSystem
dummy_render = lambda block, _: Fragment(block.data) # pylint: disable=invalid-name
@skip_unless_cms
class LibraryContentTest(MixedSplitTestCase):
"""
Base class for tests of LibraryContentBlock (library_content_block.py)
@@ -36,7 +40,7 @@ class LibraryContentTest(MixedSplitTestCase):
def setUp(self):
super().setUp()
self.user_id = UserFactory().id
self.tools = LibraryToolsService(self.store, self.user_id)
self.library = LibraryFactory.create(modulestore=self.store)
self.lib_blocks = [
@@ -53,13 +57,25 @@ class LibraryContentTest(MixedSplitTestCase):
max_count=1,
source_library_id=str(self.library.location.library_key)
)
self.lc_block.runtime._services.update({'library_tools': self.tools}) # pylint: disable=protected-access
def _sync_lc_block_from_library(self, upgrade_to_latest=False):
"""
Save the lc_block, then sync its children with the library, and then re-load it.
We must re-load it because the syncing happens in a Celery task, so that original self.lc_block instance will
not have changes manifested on it, but the re-loaded instance will.
"""
self.store.update_item(self.lc_block, self.user_id)
self.lc_block.sync_from_library(upgrade_to_latest=upgrade_to_latest)
self.lc_block = self.store.get_item(self.lc_block.location)
def _bind_course_block(self, block):
"""
Bind a block (part of self.course) so we can access student-specific data.
"""
prepare_block_runtime(block.runtime, course_id=block.location.course_key)
block.runtime._services.update({'library_tools': self.tools}) # lint-amnesty, pylint: disable=protected-access
block.runtime._services.update({'library_tools': self.tools}) # pylint: disable=protected-access
def get_block(descriptor):
"""Mocks module_system get_block function"""
@@ -71,16 +87,59 @@ class LibraryContentTest(MixedSplitTestCase):
block.runtime.get_block_for_descriptor = get_block
@ddt.ddt
class LibraryContentGeneralTest(LibraryContentTest):
"""
Test the base functionality of the LibraryContentBlock.
"""
@ddt.data(
('library-v1:ProblemX+PR0B', LibraryLocator),
('lib:ORG:test-1', LibraryLocatorV2)
)
@ddt.unpack
def test_source_library_key(self, library_key, expected_locator_type):
"""
Test the source_library_key property of the xblock.
The method should correctly work either with V1 or V2 libraries.
"""
library = self.make_block(
"library_content",
self.vertical,
max_count=1,
source_library_id=library_key
)
assert isinstance(library.source_library_key, expected_locator_type)
def test_initial_sync_from_library(self):
"""
Test that a lc block starts without children, but is correctly populated upon first sync.
"""
source_library_key = self.library.location.library_key
# Normally the children get added when the "source_libraries" setting
# is updated, but the way we do it through a factory doesn't do that.
assert self.lc_block.source_library_key == source_library_key
assert self.lc_block.source_library_version is None
assert len(self.lc_block.children) == 0
# Update the LibraryContent block's children:
self._sync_lc_block_from_library()
# Check that all blocks from the library are now children of the block:
assert self.lc_block.source_library_key == source_library_key # Unchanged
assert self.lc_block.source_library_version == self.tools.get_latest_library_version(source_library_key)
assert len(self.lc_block.children) == len(self.lib_blocks)
class TestLibraryContentExportImport(LibraryContentTest):
"""
Export and import tests for LibraryContentBlock
"""
def setUp(self):
super().setUp()
# Children will only set after calling this.
self.lc_block.refresh_children()
self.lc_block = self.store.get_item(self.lc_block.location)
self._sync_lc_block_from_library()
self.expected_olx = (
'<library_content display_name="{block.display_name}" max_count="{block.max_count}"'
@@ -177,6 +236,10 @@ class LibraryContentBlockTestMixin:
problem_type_lookup = {}
def setUp(self):
super().setUp()
self._sync_lc_block_from_library()
def _get_capa_problem_type_xml(self, *args):
""" Helper function to create empty CAPA problem definition """
problem = "<problem>"
@@ -185,7 +248,7 @@ class LibraryContentBlockTestMixin:
problem += "</problem>"
return problem
def _create_capa_problems(self):
def _add_problems_to_library(self):
"""
Helper function to create a set of capa problems to test against.
@@ -196,26 +259,10 @@ class LibraryContentBlockTestMixin:
block = self.make_block("problem", self.library, data=self._get_capa_problem_type_xml(*problem_type))
self.problem_type_lookup[block.location] = problem_type
def test_lib_content_block(self):
"""
Test that blocks from a library are copied and added as children
"""
# Check that the LibraryContent block has no children initially
# Normally the children get added when the "source_libraries" setting
# is updated, but the way we do it through a factory doesn't do that.
assert len(self.lc_block.children) == 0
# Update the LibraryContent block:
self.lc_block.refresh_children()
self.lc_block = self.store.get_item(self.lc_block.location)
# Check that all blocks from the library are now children of the block:
assert len(self.lc_block.children) == len(self.lib_blocks)
def test_children_seen_by_a_user(self):
"""
Test that each student sees only one block as a child of the LibraryContent block.
"""
self.lc_block.refresh_children()
self.lc_block = self.store.get_item(self.lc_block.location)
self._bind_course_block(self.lc_block)
# Make sure the runtime knows that the block's children vary per-user:
assert self.lc_block.has_dynamic_children()
@@ -234,93 +281,113 @@ class LibraryContentBlockTestMixin:
"""
# When source_library_id is blank, the validation summary should say this block needs to be configured:
self.lc_block.source_library_id = ""
self.lc_block.source_library_version = None
result = self.lc_block.validate()
assert not result
# Validation fails due to at least one warning/message
assert result.summary
assert StudioValidationMessage.NOT_CONFIGURED == result.summary.type
# When source_library_id references a non-existent library, we should get an error:
self.lc_block.source_library_id = "library-v1:BAD+WOLF"
self.lc_block.source_library_version = None
result = self.lc_block.validate()
assert not result
# Validation fails due to at least one warning/message
assert result.summary
assert StudioValidationMessage.ERROR == result.summary.type
assert 'invalid' in result.summary.text
# When source_library_id is set but the block needs to be updated, the summary should say so:
# When source_library_id is set but the block hasn't been synced, the summary should say so:
self.lc_block.source_library_id = str(self.library.location.library_key)
self.lc_block.source_library_version = None
result = self.lc_block.validate()
assert not result
# Validation fails due to at least one warning/message
assert result.summary
assert StudioValidationMessage.WARNING == result.summary.type
assert 'out of date' in result.summary.text
# Now if we update the block, all validation should pass:
self.lc_block.refresh_children()
self._sync_lc_block_from_library()
assert self.lc_block.validate()
# But updating the library will cause it to fail again as out-of-date:
self._add_problems_to_library()
result = self.lc_block.validate()
assert not result
assert result.summary
assert StudioValidationMessage.WARNING == result.summary.type
assert 'out of date' in result.summary.text
# And a regular sync will not fix that:
self._sync_lc_block_from_library()
result = self.lc_block.validate()
assert not result
assert result.summary
assert StudioValidationMessage.WARNING == result.summary.type
assert 'out of date' in result.summary.text
# But a upgrade_to_latest sync will:
self._sync_lc_block_from_library(upgrade_to_latest=True)
assert self.lc_block.validate()
def _assert_has_only_N_matching_problems(self, result, n):
assert result.summary
assert StudioValidationMessage.WARNING == result.summary.type
assert f'only {n} matching problem' in result.summary.text
def test_validation_of_matching_blocks(self):
"""
Test that the validation method of LibraryContent blocks can warn
the user about problems with other settings (max_count and capa_type).
"""
# Ensure we're starting wtih clean validation
assert self.lc_block.validate()
# Set max_count to higher value than exists in library
self.lc_block.max_count = 50
# In the normal studio editing process, editor_saved() calls refresh_children at this point
self.lc_block.refresh_children()
result = self.lc_block.validate()
assert not result
# Validation fails due to at least one warning/message
assert result.summary
assert StudioValidationMessage.WARNING == result.summary.type
assert 'only 4 matching problems' in result.summary.text
self._assert_has_only_N_matching_problems(result, 4)
assert len(self.lc_block.selected_children()) == 4
# Add some capa problems so we can check problem type validation messages
self._add_problems_to_library()
self._sync_lc_block_from_library(upgrade_to_latest=True)
self.lc_block.max_count = 1
self._create_capa_problems()
self.lc_block.refresh_children()
assert self.lc_block.validate()
assert len(self.lc_block.selected_children()) == 1
# Existing problem type should pass validation
self.lc_block.max_count = 1
self.lc_block.capa_type = 'multiplechoiceresponse'
self.lc_block.refresh_children()
self._sync_lc_block_from_library()
self.lc_block.max_count = 1
assert self.lc_block.validate()
assert len(self.lc_block.selected_children()) == 1
# ... unless requested more blocks than exists in library
self.lc_block.max_count = 10
self.lc_block.capa_type = 'multiplechoiceresponse'
self.lc_block.refresh_children()
self._sync_lc_block_from_library()
self.lc_block.max_count = 10
result = self.lc_block.validate()
assert not result
# Validation fails due to at least one warning/message
assert result.summary
assert StudioValidationMessage.WARNING == result.summary.type
assert 'only 1 matching problem' in result.summary.text
self._assert_has_only_N_matching_problems(result, 1)
assert len(self.lc_block.selected_children()) == 1
# Missing problem type should always fail validation
self.lc_block.max_count = 1
self.lc_block.capa_type = 'customresponse'
self.lc_block.refresh_children()
self._sync_lc_block_from_library()
self.lc_block.max_count = 1
result = self.lc_block.validate()
assert not result
# Validation fails due to at least one warning/message
assert result.summary
assert StudioValidationMessage.WARNING == result.summary.type
assert 'no matching problem types' in result.summary.text
assert 'There are no problems in the specified library of type customresponse' in result.summary.text
assert len(self.lc_block.selected_children()) == 0
# -1 selects all blocks from the library.
self.lc_block.max_count = -1
self.lc_block.capa_type = ANY_CAPA_TYPE_VALUE
self.lc_block.refresh_children()
self._sync_lc_block_from_library()
self.lc_block.max_count = -1
assert self.lc_block.validate()
assert len(self.lc_block.selected_children()) == len(self.lc_block.children)
@@ -328,27 +395,28 @@ class LibraryContentBlockTestMixin:
"""
Test that the capa type filter is actually filtering children
"""
self._create_capa_problems()
assert len(self.lc_block.children) == 0
# precondition check
self._add_problems_to_library()
self._sync_lc_block_from_library(upgrade_to_latest=True)
assert self.lc_block.children
assert len(self.lc_block.children) == len(self.library.children)
self.lc_block.capa_type = "multiplechoiceresponse"
self.lc_block.refresh_children()
self._sync_lc_block_from_library()
assert len(self.lc_block.children) == 1
self.lc_block.capa_type = "optionresponse"
self.lc_block.refresh_children()
self._sync_lc_block_from_library()
assert len(self.lc_block.children) == 3
self.lc_block.capa_type = "coderesponse"
self.lc_block.refresh_children()
self._sync_lc_block_from_library()
assert len(self.lc_block.children) == 2
self.lc_block.capa_type = "customresponse"
self.lc_block.refresh_children()
assert len(self.lc_block.children) == 0
self._sync_lc_block_from_library()
self.lc_block.capa_type = ANY_CAPA_TYPE_VALUE
self.lc_block.refresh_children()
self._sync_lc_block_from_library()
assert len(self.lc_block.children) == (len(self.lib_blocks) + 4)
def test_non_editable_settings(self):
@@ -367,24 +435,22 @@ class LibraryContentBlockTestMixin:
blocks_seen = set()
total_tries, max_tries = 0, 100
self.lc_block.refresh_children()
self.lc_block = self.store.get_item(self.lc_block.location)
self._bind_course_block(self.lc_block)
# Eventually, we should see every child block selected
while len(blocks_seen) != len(self.lib_blocks):
self._change_count_and_refresh_children(len(self.lib_blocks))
self._change_count_and_reselect_children(len(self.lib_blocks))
# Now set the number of selections to 1
selected = self._change_count_and_refresh_children(1)
selected = self._change_count_and_reselect_children(1)
blocks_seen.update(selected)
total_tries += 1
if total_tries >= max_tries:
assert False, "Max tries exceeded before seeing all blocks."
break
def _change_count_and_refresh_children(self, count):
def _change_count_and_reselect_children(self, count):
"""
Helper method that changes the max_count of self.lc_block, refreshes
Helper method that changes the max_count of self.lc_block, reselects
children, and asserts that the number of selected children equals the count provided.
"""
self.lc_block.max_count = count
@@ -413,9 +479,8 @@ class LibraryContentBlockTestMixin:
self.lc_block.allow_resetting_children = allow_resetting_children
self.lc_block.max_count = max_count
# Add some capa blocks
self._create_capa_problems()
self.lc_block.refresh_children()
self.lc_block = self.store.get_item(self.lc_block.location)
self._add_problems_to_library()
self._sync_lc_block_from_library(upgrade_to_latest=True)
# Mock the student view to return an empty dict to be returned as response
self.lc_block.student_view = MagicMock()
self.lc_block.student_view.return_value.content = {}
@@ -434,19 +499,10 @@ class LibraryContentBlockTestMixin:
assert response.status_code == status.HTTP_400_BAD_REQUEST
@patch('xmodule.library_tools.SearchEngine.get_search_engine', Mock(return_value=None, autospec=True))
class TestLibraryContentBlockNoSearchIndex(LibraryContentBlockTestMixin, LibraryContentTest):
"""
Tests for library container when no search index is available.
Tests fallback low-level CAPA problem introspection
"""
pass # pylint:disable=unnecessary-pass
search_index_mock = Mock(spec=SearchEngine) # pylint: disable=invalid-name
@patch('xmodule.library_tools.SearchEngine.get_search_engine', Mock(return_value=search_index_mock, autospec=True))
@patch.object(SearchEngine, 'get_search_engine', Mock(return_value=None, autospec=True))
class TestLibraryContentBlockWithSearchIndex(LibraryContentBlockTestMixin, LibraryContentTest):
"""
Tests for library container with mocked search engine response.
@@ -481,10 +537,12 @@ class TestLibraryContentRender(LibraryContentTest):
Rendering unit tests for LibraryContentBlock
"""
def setUp(self):
super().setUp()
self._sync_lc_block_from_library()
def test_preview_view(self):
""" Test preview view rendering """
self.lc_block.refresh_children()
self.lc_block = self.store.get_item(self.lc_block.location)
assert len(self.lc_block.children) == len(self.lib_blocks)
self._bind_course_block(self.lc_block)
rendered = self.lc_block.render(AUTHOR_VIEW, {'root_xblock': self.lc_block})
@@ -492,8 +550,6 @@ class TestLibraryContentRender(LibraryContentTest):
def test_author_view(self):
""" Test author view rendering """
self.lc_block.refresh_children()
self.lc_block = self.store.get_item(self.lc_block.location)
assert len(self.lc_block.children) == len(self.lib_blocks)
self._bind_course_block(self.lc_block)
rendered = self.lc_block.render(AUTHOR_VIEW, {})
@@ -511,8 +567,7 @@ class TestLibraryContentAnalytics(LibraryContentTest):
def setUp(self):
super().setUp()
self.publisher = Mock()
self.lc_block.refresh_children()
self.lc_block = self.store.get_item(self.lc_block.location)
self._sync_lc_block_from_library()
self._bind_course_block(self.lc_block)
self.lc_block.runtime.publish = self.publisher
@@ -582,10 +637,9 @@ class TestLibraryContentAnalytics(LibraryContentTest):
inner_vertical = self.make_block("vertical", main_vertical)
html_block = self.make_block("html", inner_vertical)
problem_block = self.make_block("problem", inner_vertical)
self.lc_block.refresh_children()
# Reload lc_block and set it up for a student:
self.lc_block = self.store.get_item(self.lc_block.location)
self._sync_lc_block_from_library(upgrade_to_latest=True)
self._bind_course_block(self.lc_block)
self.lc_block.runtime.publish = self.publisher
@@ -645,6 +699,7 @@ class TestLibraryContentAnalytics(LibraryContentTest):
Test the "removed" event emitted when we un-assign blocks previously assigned to a student.
We go from two blocks assigned, to one because the others have been deleted from the library.
"""
# Start by assigning two blocks to the student:
self.lc_block.get_child_blocks() # This line is needed in the test environment or the change has no effect
self.lc_block.max_count = 2
@@ -654,13 +709,18 @@ class TestLibraryContentAnalytics(LibraryContentTest):
# Now make sure that one of the assigned blocks will have to be un-assigned.
# To cause an "invalid" event, we delete all blocks from the content library
# except for one of the two already assigned to the student:
keep_block_key = initial_blocks_assigned[0].location
keep_block_lib_usage_key, keep_block_lib_version = self.store.get_block_original_usage(keep_block_key)
assert keep_block_lib_usage_key is not None
deleted_block_key = initial_blocks_assigned[1].location
self.library.children = [keep_block_lib_usage_key]
self.store.update_item(self.library, self.user_id)
self.lc_block.refresh_children()
self.store.update_item(self.lc_block, self.user_id)
old_selected = self.lc_block.selected
self._sync_lc_block_from_library(upgrade_to_latest=True)
self.lc_block.selected = old_selected
self.lc_block.runtime.publish = self.publisher
# Check that the event says that one block was removed, leaving one block left:
children = self.lc_block.get_child_blocks()

View File

@@ -1,39 +1,64 @@
"""
Tests for library tools service (only used by CMS)
"""
from unittest.mock import patch
Currently, the only known user of the LibraryToolsService is the
LibraryContentBlock, so these tests are all written with only that
block type in mind.
"""
from unittest import mock
import ddt
from django.conf import settings
from django.test import override_settings
from opaque_keys.edx.keys import UsageKey
from opaque_keys.edx.locator import LibraryLocator, LibraryLocatorV2
from common.djangoapps.student.roles import CourseInstructorRole
from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangolib.testing.utils import skip_unless_cms
from openedx.core.djangoapps.content_libraries import api as library_api
from openedx.core.djangoapps.content_libraries.tests.base import ContentLibrariesRestApiTest
from openedx.core.djangoapps.xblock.api import load_block
from common.djangoapps.student.roles import CourseInstructorRole
from xmodule.library_tools import LibraryToolsService
from xmodule.modulestore.tests.factories import CourseFactory, LibraryFactory
from xmodule.modulestore.tests.utils import MixedSplitTestCase
@skip_unless_cms
class LibraryToolsServiceTest(MixedSplitTestCase):
"""
Tests for library service.
@ddt.ddt
class ContentLibraryToolsTest(MixedSplitTestCase, ContentLibrariesRestApiTest):
"""
Tests for LibraryToolsService.
Tests interaction with blockstore-based (V2) and mongo-based (V1) content libraries.
"""
def setUp(self):
super().setUp()
UserFactory(is_staff=True, id=self.user_id)
self.tools = LibraryToolsService(self.store, self.user_id)
def test_list_available_libraries(self):
"""
Test listing of libraries.
Collects Only V2 Libaries if the FEATURES[ENABLE_LIBRARY_AUTHORING_MICROFRONTEND] setting is True.
Otherwise, return all v1 and v2 libraries.
"""
# create V1 library
_ = LibraryFactory.create(modulestore=self.store)
# create V2 library
self._create_library(slug="testlib1_preview", title="Test Library 1", description="Testing XBlocks")
all_libraries = self.tools.list_available_libraries()
assert all_libraries
assert len(all_libraries) == 1
assert len(all_libraries) == 2
@patch('xmodule.modulestore.split_mongo.split.SplitMongoModuleStore.get_library_summaries')
with override_settings(FEATURES={**settings.FEATURES, "ENABLE_LIBRARY_AUTHORING_MICROFRONTEND": True}):
all_libraries = self.tools.list_available_libraries()
assert all_libraries
assert len(all_libraries) == 1
@mock.patch('xmodule.modulestore.split_mongo.split.SplitMongoModuleStore.get_library_summaries')
def test_list_available_libraries_fetch(self, mock_get_library_summaries):
"""
Test that library list is compiled using light weight library summary objects.
@@ -41,17 +66,75 @@ class LibraryToolsServiceTest(MixedSplitTestCase):
_ = self.tools.list_available_libraries()
assert mock_get_library_summaries.called
def test_get_latest_v1_library_version(self):
"""
Test get_v1_library_version for V1 libraries.
@skip_unless_cms
class ContentLibraryToolsTest(MixedSplitTestCase, ContentLibrariesRestApiTest):
"""
Tests for LibraryToolsService which interact with blockstore-based content libraries
"""
def setUp(self):
super().setUp()
self.tools = LibraryToolsService(self.store, self.user.id)
Covers getting results for either string library key or LibraryLocator.
"""
lib_key = LibraryFactory.create(modulestore=self.store).location.library_key
# Re-load the library from the modulestore, explicitly including version information:
lib = self.store.get_library(lib_key, remove_version=False, remove_branch=False)
# check the result using the LibraryLocator
assert isinstance(lib_key, LibraryLocator)
result = self.tools.get_latest_library_version(lib_key)
assert result
assert result == str(lib.location.library_key.version_guid)
# the same check for string representation of the LibraryLocator
str_key = str(lib_key)
result = self.tools.get_latest_library_version(str_key)
assert result
assert result == str(lib.location.library_key.version_guid)
def test_import_from_blockstore(self):
@ddt.data(
'library-v1:Fake+Key', # V1 library key
'lib:Fake:V-2', # V2 library key
LibraryLocator.from_string('library-v1:Fake+Key'),
LibraryLocatorV2.from_string('lib:Fake:V-2'),
)
def test_get_latest_library_version_no_library(self, lib_key):
"""
Test get_latest_library_version result when the library does not exist.
Provided lib_key's are valid V1 or V2 keys.
"""
assert self.tools.get_latest_library_version(lib_key) is None
def test_update_children_for_v2_lib(self):
"""
Test update_children with V2 library as a source.
"""
library = self._create_library(
slug="cool-v2-lib", title="The best Library", description="Spectacular description"
)
self._add_block_to_library(library["id"], "unit", "unit1_id")
course = CourseFactory.create(modulestore=self.store, user_id=self.user.id)
CourseInstructorRole(course.id).add_users(self.user)
content_block = self.make_block(
"library_content",
course,
max_count=1,
source_library_id=library['id']
)
assert len(content_block.children) == 0
# Populate children from library
self.tools.trigger_library_sync(content_block, library_version=None)
# The updates happen in a Celery task, so this particular content_block instance is no updated.
# We must re-instantiate it from modulstore in order to see the updated children list.
content_block = self.store.get_item(content_block.location)
assert len(content_block.children) == 1
def test_update_children_for_v2_lib_recursive(self):
"""
Test update_children for a V2 library containing a unit.
Ensures that _import_from_blockstore works on nested blocks.
"""
# Create a blockstore content library
library = self._create_library(slug="testlib1_import", title="A Test Library", description="Testing XBlocks")
# Create a unit block with an HTML block in it.
@@ -66,10 +149,17 @@ class ContentLibraryToolsTest(MixedSplitTestCase, ContentLibrariesRestApiTest):
course = CourseFactory.create(modulestore=self.store, user_id=self.user.id)
CourseInstructorRole(course.id).add_users(self.user)
# Add Source from library block to the course
lc_block = self.make_block("library_content", course, user_id=self.user.id)
lc_block = self.make_block(
"library_content",
course,
user_id=self.user_id,
max_count=1,
source_library_id=str(library["id"]),
)
# Import the unit block from the library to the course
self.tools.import_from_blockstore(lc_block, [unit_block_id])
self.tools.trigger_library_sync(lc_block, library_version=None)
lc_block = self.store.get_item(lc_block.location)
# Verify imported block with its children
assert len(lc_block.children) == 1
@@ -89,7 +179,8 @@ class ContentLibraryToolsTest(MixedSplitTestCase, ContentLibrariesRestApiTest):
# Check that reimporting updates the target block
self._set_library_block_olx(html_block_id, '<html><a href="/static/test.txt">Foo bar</a></html>')
self.tools.import_from_blockstore(lc_block, [unit_block_id])
self.tools.trigger_library_sync(lc_block, library_version=None)
lc_block = self.store.get_item(lc_block.location)
assert len(lc_block.children) == 1
imported_unit_block = self.store.get_item(lc_block.children[0])
@@ -97,3 +188,24 @@ class ContentLibraryToolsTest(MixedSplitTestCase, ContentLibrariesRestApiTest):
imported_html_block = self.store.get_item(imported_unit_block.children[0])
assert 'Hello world' not in imported_html_block.data
assert 'Foo bar' in imported_html_block.data
def test_update_children_for_v1_lib(self):
"""
Test update_children with V1 library as a source.
As for now, covers usage of update_children for the library content module only.
"""
library = LibraryFactory.create(modulestore=self.store)
self.make_block("html", library, data="Hello world from the block")
course = CourseFactory.create(modulestore=self.store)
content_block = self.make_block(
"library_content",
course,
max_count=1,
source_library_id=str(library.location.library_key)
)
assert len(content_block.children) == 0
self.tools.trigger_library_sync(content_block, library_version=None)
content_block = self.store.get_item(content_block.location)
assert len(content_block.children) == 1