This commit leaves behind just enough Old Mongo (DraftModulestore) functionality to allow read-only access to static assets and the root CourseBlock. It removes: * create/update operations * child/parent traversal * inheritance related code It also removes or converts tests for this functionality. The ability to read from the root CourseBlock was maintained for backwards compatibility, since top-level course settings are often stored here, and this is used by various parts of the codebase, like displaying dashboards and re-building CourseOverview models. Any attempt to read the contents of a course by getting the CourseBlock's children will return an empty list (i.e. it will look empty). This commit does _not_ delete content on MongoDB or run any sort of data migration or cleanup.
260 lines
9.7 KiB
Python
260 lines
9.7 KiB
Python
"""
|
|
Tests for XML importer.
|
|
"""
|
|
|
|
|
|
import importlib
|
|
import os
|
|
import unittest
|
|
from uuid import uuid4
|
|
from unittest import mock
|
|
|
|
from opaque_keys.edx.keys import CourseKey
|
|
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
|
|
from path import Path as path
|
|
from xblock.fields import List, Scope, ScopeIds, String
|
|
from xblock.runtime import DictKeyValueStore, KvsFieldData, Runtime
|
|
|
|
from xmodule.modulestore import ModuleStoreEnum
|
|
from xmodule.modulestore.inheritance import InheritanceMixin
|
|
from xmodule.modulestore.tests.mongo_connection import MONGO_HOST, MONGO_PORT_NUM
|
|
from xmodule.modulestore.xml_importer import StaticContentImporter, _update_block_location
|
|
from xmodule.tests import DATA_DIR
|
|
from xmodule.x_module import XModuleMixin
|
|
|
|
OPEN_BUILTIN = 'builtins.open'
|
|
|
|
|
|
class ModuleStoreNoSettings(unittest.TestCase):
|
|
"""
|
|
A mixin to create a mongo modulestore that avoids settings
|
|
"""
|
|
HOST = MONGO_HOST
|
|
PORT = MONGO_PORT_NUM
|
|
DB = 'test_mongo_%s' % uuid4().hex[:5]
|
|
COLLECTION = 'modulestore'
|
|
FS_ROOT = DATA_DIR
|
|
DEFAULT_CLASS = 'xmodule.modulestore.tests.test_xml_importer.StubXBlock'
|
|
RENDER_TEMPLATE = lambda t_n, d, ctx=None, nsp='main': ''
|
|
|
|
modulestore_options = {
|
|
'default_class': DEFAULT_CLASS,
|
|
'fs_root': DATA_DIR,
|
|
'render_template': RENDER_TEMPLATE,
|
|
}
|
|
DOC_STORE_CONFIG = {
|
|
'host': HOST,
|
|
'port': PORT,
|
|
'db': DB,
|
|
'collection': COLLECTION,
|
|
}
|
|
MODULESTORE = {
|
|
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
|
|
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
|
|
'OPTIONS': modulestore_options
|
|
}
|
|
|
|
modulestore = None
|
|
|
|
def cleanup_modulestore(self):
|
|
"""
|
|
cleanup
|
|
"""
|
|
if self.modulestore:
|
|
self.modulestore._drop_database() # pylint: disable=protected-access
|
|
|
|
def setUp(self):
|
|
"""
|
|
Add cleanups
|
|
"""
|
|
self.addCleanup(self.cleanup_modulestore)
|
|
super().setUp()
|
|
|
|
|
|
#===========================================
|
|
def modulestore():
|
|
"""
|
|
Mock the django dependent global modulestore function to disentangle tests from django
|
|
"""
|
|
def load_function(engine_path):
|
|
"""
|
|
Load the given engine
|
|
"""
|
|
module_path, _, name = engine_path.rpartition('.')
|
|
return getattr(importlib.import_module(module_path), name)
|
|
|
|
if ModuleStoreNoSettings.modulestore is None:
|
|
class_ = load_function(ModuleStoreNoSettings.MODULESTORE['ENGINE'])
|
|
|
|
options = {}
|
|
|
|
options.update(ModuleStoreNoSettings.MODULESTORE['OPTIONS'])
|
|
options['render_template'] = render_to_template_mock
|
|
|
|
# lint-amnesty, pylint: disable=bad-option-value, star-args
|
|
ModuleStoreNoSettings.modulestore = class_(
|
|
None, # contentstore
|
|
ModuleStoreNoSettings.MODULESTORE['DOC_STORE_CONFIG'],
|
|
branch_setting_func=lambda: ModuleStoreEnum.Branch.draft_preferred,
|
|
**options
|
|
)
|
|
|
|
return ModuleStoreNoSettings.modulestore
|
|
|
|
|
|
# pylint: disable=unused-argument
|
|
def render_to_template_mock(*args):
|
|
pass
|
|
|
|
|
|
class StubXBlock(XModuleMixin, InheritanceMixin):
|
|
"""
|
|
Stub XBlock used for testing.
|
|
"""
|
|
test_content_field = String(
|
|
help="A content field that will be explicitly set",
|
|
scope=Scope.content,
|
|
default="default value"
|
|
)
|
|
|
|
test_settings_field = String(
|
|
help="A settings field that will be explicitly set",
|
|
scope=Scope.settings,
|
|
default="default value"
|
|
)
|
|
|
|
|
|
class StubXBlockWithMutableFields(StubXBlock):
|
|
"""
|
|
Stub XBlock used for testing mutable fields and children
|
|
"""
|
|
has_children = True
|
|
|
|
test_mutable_content_field = List(
|
|
help="A mutable content field that will be explicitly set",
|
|
scope=Scope.content,
|
|
)
|
|
|
|
test_mutable_settings_field = List(
|
|
help="A mutable settings field that will be explicitly set",
|
|
scope=Scope.settings,
|
|
)
|
|
|
|
|
|
class UpdateLocationTest(ModuleStoreNoSettings):
|
|
"""
|
|
Test that updating location preserves "is_set_on" status on fields
|
|
"""
|
|
CONTENT_FIELDS = ['test_content_field', 'test_mutable_content_field']
|
|
SETTINGS_FIELDS = ['test_settings_field', 'test_mutable_settings_field']
|
|
CHILDREN_FIELDS = ['children']
|
|
|
|
def setUp(self):
|
|
"""
|
|
Create a stub XBlock backed by in-memory storage.
|
|
"""
|
|
self.runtime = mock.MagicMock(Runtime)
|
|
self.field_data = KvsFieldData(kvs=DictKeyValueStore())
|
|
self.scope_ids = ScopeIds('Bob', 'mutablestubxblock', '123', 'import')
|
|
self.xblock = StubXBlockWithMutableFields(self.runtime, self.field_data, self.scope_ids)
|
|
|
|
self.fake_children_locations = [
|
|
BlockUsageLocator(CourseLocator('org', 'course', 'run'), 'mutablestubxblock', 'child1'),
|
|
BlockUsageLocator(CourseLocator('org', 'course', 'run'), 'mutablestubxblock', 'child2'),
|
|
]
|
|
|
|
super().setUp()
|
|
|
|
def _check_explicitly_set(self, block, scope, expected_explicitly_set_fields, should_be_set=False):
|
|
""" Gets fields that are explicitly set on block and checks if they are marked as explicitly set or not """
|
|
actual_explicitly_set_fields = block.get_explicitly_set_fields_by_scope(scope=scope)
|
|
assertion = self.assertIn if should_be_set else self.assertNotIn
|
|
for field in expected_explicitly_set_fields:
|
|
assertion(field, actual_explicitly_set_fields)
|
|
|
|
def test_update_locations_native_xblock(self):
|
|
""" Update locations updates location and keeps values and "is_set_on" status """
|
|
# Set the XBlock's location
|
|
self.xblock.location = BlockUsageLocator(CourseLocator("org", "import", "run"), "category", "stubxblock")
|
|
|
|
# Explicitly set the content, settings and children fields
|
|
self.xblock.test_content_field = 'Explicitly set'
|
|
self.xblock.test_settings_field = 'Explicitly set'
|
|
self.xblock.test_mutable_content_field = [1, 2, 3]
|
|
self.xblock.test_mutable_settings_field = ["a", "s", "d"]
|
|
self.xblock.children = self.fake_children_locations # pylint:disable=attribute-defined-outside-init
|
|
self.xblock.save()
|
|
|
|
# Update location
|
|
target_location = self.xblock.location.replace(revision='draft')
|
|
_update_block_location(self.xblock, target_location)
|
|
new_version = self.xblock # _update_block_location updates in-place
|
|
|
|
# Check the XBlock's location
|
|
assert new_version.location == target_location
|
|
|
|
# Check the values of the fields.
|
|
# The content, settings and children fields should be preserved
|
|
assert new_version.test_content_field == 'Explicitly set'
|
|
assert new_version.test_settings_field == 'Explicitly set'
|
|
assert new_version.test_mutable_content_field == [1, 2, 3]
|
|
assert new_version.test_mutable_settings_field == ['a', 's', 'd']
|
|
assert new_version.children == self.fake_children_locations
|
|
|
|
# Expect that these fields are marked explicitly set
|
|
self._check_explicitly_set(new_version, Scope.content, self.CONTENT_FIELDS, should_be_set=True)
|
|
self._check_explicitly_set(new_version, Scope.settings, self.SETTINGS_FIELDS, should_be_set=True)
|
|
self._check_explicitly_set(new_version, Scope.children, self.CHILDREN_FIELDS, should_be_set=True)
|
|
|
|
# Expect these fields pass "is_set_on" test
|
|
for field in self.CONTENT_FIELDS + self.SETTINGS_FIELDS + self.CHILDREN_FIELDS:
|
|
assert new_version.fields[field].is_set_on(new_version) # pylint: disable=unsubscriptable-object
|
|
|
|
|
|
class StaticContentImporterTest(unittest.TestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
|
|
|
def setUp(self): # lint-amnesty, pylint: disable=super-method-not-called
|
|
self.course_data_path = path('/path')
|
|
self.mocked_content_store = mock.Mock()
|
|
self.static_content_importer = StaticContentImporter(
|
|
static_content_store=self.mocked_content_store,
|
|
course_data_path=self.course_data_path,
|
|
target_id=CourseKey.from_string('course-v1:edX+DemoX+Demo_Course')
|
|
)
|
|
|
|
def test_import_static_content_directory(self):
|
|
static_content_dir = 'static'
|
|
expected_base_dir = path(self.course_data_path / static_content_dir)
|
|
mocked_os_walk_yield = [
|
|
('static', None, ['file1.txt', 'file2.txt']),
|
|
('static/inner', None, ['file1.txt']),
|
|
]
|
|
with mock.patch(
|
|
'xmodule.modulestore.xml_importer.os.walk',
|
|
return_value=mocked_os_walk_yield
|
|
), mock.patch.object(
|
|
self.static_content_importer, 'import_static_file'
|
|
) as patched_import_static_file:
|
|
self.static_content_importer.import_static_content_directory('static')
|
|
patched_import_static_file.assert_any_call(
|
|
'static/file1.txt', base_dir=expected_base_dir
|
|
)
|
|
patched_import_static_file.assert_any_call(
|
|
'static/file2.txt', base_dir=expected_base_dir
|
|
)
|
|
patched_import_static_file.assert_any_call(
|
|
'static/inner/file1.txt', base_dir=expected_base_dir
|
|
)
|
|
|
|
def test_import_static_file(self):
|
|
base_dir = path('/path/to/dir')
|
|
full_file_path = os.path.join(base_dir, 'static/some_file.txt')
|
|
self.mocked_content_store.generate_thumbnail.return_value = (None, None)
|
|
with mock.patch(OPEN_BUILTIN, mock.mock_open(read_data=b"data")) as mock_file:
|
|
self.static_content_importer.import_static_file(
|
|
full_file_path=full_file_path,
|
|
base_dir=base_dir
|
|
)
|
|
mock_file.assert_called_with(full_file_path, 'rb')
|
|
self.mocked_content_store.generate_thumbnail.assert_called_once()
|