Files

364 lines
14 KiB
Python

# pylint: disable=protected-access
"""
Tests for import_course_from_xml using the mongo modulestore.
"""
import copy
from unittest import skip
from unittest.mock import patch
from uuid import uuid4
import ddt
from django.conf import settings
from django.test.client import Client
from django.test.utils import override_settings
from django.core.files.storage import storages
from xmodule.contentstore.django import contentstore
from xmodule.exceptions import NotFoundError
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.xml_importer import import_course_from_xml
from common.djangoapps.util.storage import resolve_storage_backend
from storages.backends.s3boto3 import S3Boto3Storage
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
@ddt.ddt
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE, SEARCH_ENGINE=None)
class ContentStoreImportTest(ModuleStoreTestCase):
"""
Tests that rely on the toy and test_import_course courses.
NOTE: refactor using CourseFactory so they do not.
"""
def setUp(self):
super().setUp()
self.client = Client()
self.client.login(username=self.user.username, password=self.user_password)
# block_structure.update_course_in_cache cannot succeed in tests, as it needs to be run async on an lms worker
self.task_patcher = patch('openedx.core.djangoapps.content.block_structure.tasks.update_course_in_cache_v2')
self._mock_lms_task = self.task_patcher.start()
def tearDown(self):
self.task_patcher.stop()
super().tearDown()
def load_test_import_course(self, target_id=None, create_if_not_present=True, module_store=None):
'''
Load the standard course used to test imports
(for do_import_static=False behavior).
'''
content_store = contentstore()
if module_store is None:
module_store = modulestore()
import_course_from_xml(
module_store,
self.user.id,
TEST_DATA_DIR,
['test_import_course'],
static_content_store=content_store,
do_import_static=False,
verbose=True,
target_id=target_id,
create_if_not_present=create_if_not_present,
)
course_id = module_store.make_course_key('edX', 'test_import_course', '2012_Fall')
course = module_store.get_course(course_id)
self.assertIsNotNone(course)
return module_store, content_store, course
def test_import_course_into_similar_namespace(self):
# Checks to make sure that a course with an org/course like
# edx/course can be imported into a namespace with an org/course
# like edx/course_name
module_store, __, course = self.load_test_import_course()
course_items = import_course_from_xml(
module_store,
self.user.id,
TEST_DATA_DIR,
['test_import_course_2'],
target_id=course.id,
verbose=True,
)
self.assertEqual(len(course_items), 1)
def test_unicode_chars_in_course_name_import(self):
"""
# Test that importing course with unicode 'id' and 'display name' doesn't give UnicodeEncodeError
"""
# Test with the split modulestore because store.has_course fails in old mongo with unicode characters.
with modulestore().default_store(ModuleStoreEnum.Type.split):
module_store = modulestore()
course_id = module_store.make_course_key('Юникода', 'unicode_course', 'échantillon')
import_course_from_xml(
module_store,
self.user.id,
TEST_DATA_DIR,
['2014_Uni'],
target_id=course_id,
create_if_not_present=True
)
course = module_store.get_course(course_id)
self.assertIsNotNone(course)
# test that course 'display_name' same as imported course 'display_name'
self.assertEqual(course.display_name, "Φυσικά το όνομα Unicode")
def test_static_import(self):
'''
Stuff in static_import should always be imported into contentstore
'''
_, content_store, course = self.load_test_import_course()
# make sure we have ONE asset in our contentstore ("should_be_imported.html")
all_assets, count = content_store.get_all_content_for_course(course.id)
print("len(all_assets)=%d" % len(all_assets))
self.assertEqual(len(all_assets), 1)
self.assertEqual(count, 1)
content = None
try:
location = course.id.make_asset_key('asset', 'should_be_imported.html')
content = content_store.find(location)
except NotFoundError:
pass
self.assertIsNotNone(content)
# make sure course.static_asset_path is correct
print(f"static_asset_path = {course.static_asset_path}")
self.assertEqual(course.static_asset_path, 'test_import_course')
def test_asset_import_nostatic(self):
'''
This test validates that an image asset is NOT imported when do_import_static=False
'''
content_store = contentstore()
module_store = modulestore()
import_course_from_xml(
module_store, self.user.id, TEST_DATA_DIR, ['toy'],
static_content_store=content_store, do_import_static=False,
do_import_python_lib=False, # python_lib.zip is special-cased -- exclude it too
create_if_not_present=True, verbose=True
)
course = module_store.get_course(module_store.make_course_key('edX', 'toy', '2012_Fall'))
# make sure we have NO assets in our contentstore
all_assets, count = content_store.get_all_content_for_course(course.id)
self.assertEqual(all_assets, [])
self.assertEqual(count, 0)
def test_no_static_link_rewrites_on_import(self):
module_store = modulestore()
courses = import_course_from_xml(
module_store, self.user.id, TEST_DATA_DIR, ['toy'], do_import_static=False, verbose=True,
create_if_not_present=True
)
course_key = courses[0].id
handouts = module_store.get_item(course_key.make_usage_key('course_info', 'handouts'))
self.assertIn('/static/', handouts.data)
handouts = module_store.get_item(course_key.make_usage_key('html', 'toyhtml'))
self.assertIn('/static/', handouts.data)
def test_tab_name_imports_correctly(self):
_module_store, _content_store, course = self.load_test_import_course()
print(f"course tabs = {course.tabs}")
self.assertEqual(course.tabs[1]['name'], 'Syllabus')
def test_reimport(self):
__, __, course = self.load_test_import_course(create_if_not_present=True)
self.load_test_import_course(target_id=course.id)
@skip("OldMongo Deprecation")
def test_rewrite_reference_list(self):
# This test fails with split modulestore (the HTML component is not in "different_course_id" namespace).
# More investigation needs to be done.
module_store = modulestore()
target_id = module_store.make_course_key('testX', 'conditional_copy', 'copy_run')
import_course_from_xml(
module_store,
self.user.id,
TEST_DATA_DIR,
['conditional'],
target_id=target_id,
create_if_not_present=True
)
conditional_block = module_store.get_item(
target_id.make_usage_key('conditional', 'condone')
)
self.assertIsNotNone(conditional_block)
different_course_id = module_store.make_course_key('edX', 'different_course', 'course_run')
self.assertListEqual(
[
target_id.make_usage_key('problem', 'choiceprob'),
different_course_id.make_usage_key('html', 'for_testing_import_rewrites')
],
conditional_block.sources_list
)
self.assertListEqual(
[
target_id.make_usage_key('html', 'congrats'),
target_id.make_usage_key('html', 'secret_page')
],
conditional_block.show_tag_list
)
def test_rewrite_reference_value_dict_published(self):
"""
Test rewriting references in ReferenceValueDict, specifically with published content.
"""
self._verify_split_test_import(
'split_test_copy',
'split_test_block',
'split1',
{"0": 'sample_0', "2": 'sample_2'},
)
def test_rewrite_reference_value_dict_draft(self):
"""
Test rewriting references in ReferenceValueDict, specifically with draft content.
"""
self._verify_split_test_import(
'split_test_copy_with_draft',
'split_test_block_draft',
'fb34c21fe64941999eaead421a8711b8',
{"0": '9f0941d021414798836ef140fb5f6841', "1": '0faf29473cf1497baa33fcc828b179cd'},
)
def _verify_split_test_import(self, target_course_name, source_course_name, split_test_name, groups_to_verticals): # lint-amnesty, pylint: disable=missing-function-docstring
module_store = modulestore()
target_id = module_store.make_course_key('testX', target_course_name, 'copy_run')
import_course_from_xml(
module_store,
self.user.id,
TEST_DATA_DIR,
[source_course_name],
target_id=target_id,
create_if_not_present=True
)
split_test_block = module_store.get_item(
target_id.make_usage_key('split_test', split_test_name)
)
self.assertIsNotNone(split_test_block)
remapped_verticals = {
key: target_id.make_usage_key('vertical', value) for key, value in groups_to_verticals.items()
}
self.assertEqual(remapped_verticals, split_test_block.group_id_to_child)
def test_video_components_present_while_import(self):
"""
Test that video components with same edx_video_id are present while re-importing
"""
module_store = modulestore()
course_id = module_store.make_course_key('edX', 'test_import_course', '2012_Fall')
# Import first time
__, __, course = self.load_test_import_course(target_id=course_id, module_store=module_store)
# Re-import
__, __, re_course = self.load_test_import_course(target_id=course.id, module_store=module_store)
vertical = module_store.get_item(re_course.id.make_usage_key('vertical', 'vertical_test'))
video = module_store.get_item(vertical.children[1])
self.assertEqual(video.display_name, 'default')
@override_settings(
COURSE_IMPORT_EXPORT_STORAGE="cms.djangoapps.contentstore.storage.ImportExportS3Storage",
STORAGES={
'default': {
'BACKEND': "django.core.files.storage.FileSystemStorage"
}
}
)
def test_default_storage(self):
""" Ensure the default storage is invoked, even if course export storage is configured """
storage = storages["default"]
self.assertEqual(storage.__class__.__name__, "FileSystemStorage")
@override_settings(
COURSE_IMPORT_EXPORT_STORAGE="cms.djangoapps.contentstore.storage.ImportExportS3Storage",
STORAGES={
'default': {
'BACKEND': "django.core.files.storage.FileSystemStorage"
}
},
COURSE_IMPORT_EXPORT_BUCKET="bucket_name_test"
)
def test_resolve_happy_path_storage(self):
""" Make sure that the correct course export storage is being used """
storage = resolve_storage_backend(
storage_key="course_import_export",
legacy_setting_key="COURSE_IMPORT_EXPORT_STORAGE"
)
self.assertEqual(storage.__class__.__name__, "ImportExportS3Storage")
self.assertEqual(storage.bucket_name, "bucket_name_test")
@override_settings()
def test_resolve_storage_with_no_config(self):
""" If no storage setup is defined, we get FileSystemStorage by default """
del settings.COURSE_IMPORT_EXPORT_STORAGE
del settings.COURSE_IMPORT_EXPORT_BUCKET
storage = resolve_storage_backend(
storage_key="course_import_export",
legacy_setting_key="COURSE_IMPORT_EXPORT_STORAGE"
)
self.assertEqual(storage.__class__.__name__, "FileSystemStorage")
@override_settings(
COURSE_IMPORT_EXPORT_STORAGE=None,
COURSE_IMPORT_EXPORT_BUCKET="bucket_name_test",
STORAGES={
'course_import_export': {
'BACKEND': 'cms.djangoapps.contentstore.storage.ImportExportS3Storage',
'OPTIONS': {}
}
}
)
def test_resolve_storage_using_django5_settings(self):
""" Simulating a Django 4 environment using Django 5 Storages configuration """
storage = resolve_storage_backend(
storage_key="course_import_export",
legacy_setting_key="COURSE_IMPORT_EXPORT_STORAGE"
)
self.assertEqual(storage.__class__.__name__, "ImportExportS3Storage")
self.assertEqual(storage.bucket_name, "bucket_name_test")
@override_settings(
STORAGES={
'course_import_export': {
'BACKEND': 'storages.backends.s3boto3.S3Boto3Storage',
'OPTIONS': {
'bucket_name': 'bucket_name_test'
}
}
}
)
def test_resolve_storage_using_django5_settings_with_options(self):
""" Ensure we call the storage class with the correct parameters and Django 5 setup """
del settings.COURSE_IMPORT_EXPORT_STORAGE
del settings.COURSE_IMPORT_EXPORT_BUCKET
storage = resolve_storage_backend(
storage_key="course_import_export",
legacy_setting_key="COURSE_IMPORT_EXPORT_STORAGE"
)
self.assertEqual(storage.__class__.__name__, S3Boto3Storage.__name__)
self.assertEqual(storage.bucket_name, "bucket_name_test")